diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index e93806cd24d..b0855f4dd12 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -182,28 +182,6 @@ Layout/HashAlignment: - 'ee/spec/support/shared_examples/status_page/publish_shared_examples.rb' - 'ee/spec/support/shared_examples/status_page/reference_links_examples.rb' - 'ee/spec/workers/scan_security_report_secrets_worker_spec.rb' - - 'lib/api/issue_links.rb' - - 'lib/api/issues.rb' - - 'lib/api/labels.rb' - - 'lib/api/maven_packages.rb' - - 'lib/api/members.rb' - - 'lib/api/merge_requests.rb' - - 'lib/api/metrics/dashboard/annotations.rb' - - 'lib/api/metrics/user_starred_dashboards.rb' - - 'lib/api/milestone_responses.rb' - - 'lib/api/notes.rb' - - 'lib/api/pages_domains.rb' - - 'lib/api/project_packages.rb' - - 'lib/api/project_templates.rb' - - 'lib/api/projects.rb' - - 'lib/api/protected_branches.rb' - - 'lib/api/releases.rb' - - 'lib/api/rubygem_packages.rb' - - 'lib/api/sidekiq_metrics.rb' - - 'lib/api/users.rb' - - 'lib/backup/gitaly_backup.rb' - - 'lib/banzai/filter/references/abstract_reference_filter.rb' - - 'lib/banzai/reference_redactor.rb' - 'lib/gitlab/abuse.rb' - 'lib/gitlab/access.rb' - 'lib/gitlab/application_rate_limiter.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index 6963e0f2aba..bb95d8f7fe9 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -118,6 +118,7 @@ Style/FormatString: - 'app/models/integrations/mattermost.rb' - 'app/models/integrations/pipelines_email.rb' - 'app/models/integrations/pivotaltracker.rb' + - 'app/models/integrations/pumble.rb' - 'app/models/integrations/pushover.rb' - 'app/models/integrations/redmine.rb' - 'app/models/integrations/unify_circuit.rb' diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js index 5eda0ff8b1a..28a50adca6b 100644 --- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js +++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js @@ -104,15 +104,16 @@ const getAttrsFactory = ({ attributeTransformer, markdown }) => function getAttrs(proseMirrorNodeSpec, hastNode, hastParents) { const { getAttrs: specGetAttrs } = proseMirrorNodeSpec; const attributes = { - ...createSourceMapAttributes(hastNode, markdown), ...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, markdown) : {}), }; + const { transform } = attributeTransformer; - return mapValues(attributes, (value, key) => - attributeTransformer.attributes.includes(key) - ? attributeTransformer.transform(value, key) - : value, - ); + return { + ...createSourceMapAttributes(hastNode, markdown), + ...mapValues(attributes, (attributeValue, attributeName) => + transform(attributeName, attributeValue, hastNode), + ), + }; }; /** diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js index b9203daf0e8..66923a4eb4d 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -1,4 +1,5 @@ import { render } from '~/lib/gfm'; +import { isValidAttribute } from '~/lib/dompurify'; import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter'; const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del']; @@ -184,28 +185,34 @@ const factorySpecs = { }, }; -const resolveUrl = (url) => { - try { - return new URL(url, window.location.origin).toString(); - } catch { +const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url']; + +const sanitizeAttribute = (attributeName, attributeValue, hastNode) => { + if (!attributeValue || SANITIZE_ALLOWLIST.includes(attributeName)) { + return attributeValue; + } + + /** + * This is a workaround to validate the value of the canonicalSrc + * attribute using DOMPurify without passing the attribute name. canonicalSrc + * is not an allowed attribute in DOMPurify therefore the library will remove + * it regardless of its value. + * + * We want to preserve canonicalSrc, and we also want to make sure that its + * value is sanitized. + */ + const validateAttributeAs = attributeName === 'canonicalSrc' ? 'src' : attributeName; + + if (!isValidAttribute(hastNode.tagName, validateAttributeAs, attributeValue)) { return null; } + + return attributeValue; }; const attributeTransformer = { - attributes: ['href', 'src'], - transform: (url) => { - if (!url) { - return url; - } - - /** - * Resolves a URL if provided. The URL is not resolved against - * the client origin initially to protect the URL protocol - * when it is available, for example, we want to preserve - * mailto and application-specific protocols - */ - return resolveUrl(url); + transform: (attributeName, attributeValue, hastNode) => { + return sanitizeAttribute(attributeName, attributeValue, hastNode); }, }; diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js index af7a1201889..3e28ca2a0f7 100644 --- a/app/assets/javascripts/lib/dompurify.js +++ b/app/assets/javascripts/lib/dompurify.js @@ -93,3 +93,5 @@ addHook('afterSanitizeAttributes', (node) => { }); export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config }); + +export { isValidAttribute } from 'dompurify'; diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb index 5601b7a7f79..53dec698fa0 100644 --- a/app/controllers/concerns/accepts_pending_invitations.rb +++ b/app/controllers/concerns/accepts_pending_invitations.rb @@ -3,12 +3,12 @@ module AcceptsPendingInvitations extend ActiveSupport::Concern - def accept_pending_invitations - return unless resource.active_for_authentication? + def accept_pending_invitations(user: resource) + return unless user.active_for_authentication? - if resource.pending_invitations.load.any? - resource.accept_pending_invitations! - clear_stored_location_for_resource + if user.pending_invitations.load.any? + user.accept_pending_invitations! + clear_stored_location_for(user: user) after_pending_invitations_hook end end @@ -17,8 +17,8 @@ module AcceptsPendingInvitations # no-op end - def clear_stored_location_for_resource - session_key = stored_location_key_for(resource) + def clear_stored_location_for(user:) + session_key = stored_location_key_for(user) session.delete(session_key) end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index bc261a95e03..817f272d458 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -6,6 +6,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController include AuthHelper include InitializesCurrentUserMode include KnownSignIn + include AcceptsPendingInvitations after_action :verify_known_sign_in @@ -159,6 +160,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def sign_in_user_flow(auth_user_class) auth_user = build_auth_user(auth_user_class) + new_user = auth_user.new? user = auth_user.find_and_update! if auth_user.valid_sign_in? @@ -178,6 +180,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.') end + accept_pending_invitations(user: user) if new_user store_after_sign_up_path_for_user if intent_to_register? sign_in_and_redirect(user, event: :authentication) end diff --git a/app/models/integration.rb b/app/models/integration.rb index 1d966ac4da0..6d755016380 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -21,7 +21,7 @@ class Integration < ApplicationRecord asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email - pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao + pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao ].freeze # TODO Shimo is temporary disabled on group and instance-levels. diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb new file mode 100644 index 00000000000..17026410eb1 --- /dev/null +++ b/app/models/integrations/pumble.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Integrations + class Pumble < BaseChatNotification + def title + 'Pumble' + end + + def description + s_("PumbleIntegration|Send notifications about project events to Pumble.") + end + + def self.to_param + 'pumble' + end + + def help + docs_link = ActionController::Base.helpers.link_to( + _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'), + target: '_blank', + rel: 'noopener noreferrer' + ) + # rubocop:disable Layout/LineLength + s_("PumbleIntegration|Send notifications about project events to Pumble. %{docs_link}") % { docs_link: docs_link.html_safe } + # rubocop:enable Layout/LineLength + end + + def default_channel_placeholder + end + + def self.supported_events + %w[push issue confidential_issue merge_request note confidential_note tag_push + pipeline wiki_page] + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: "https://api.pumble.com/workspaces/x/...", required: true }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + { + type: 'select', + name: 'branches_to_be_notified', + title: s_('Integrations|Branches for which notifications are to be sent'), + choices: self.class.branch_choices + } + ] + end + + private + + def notify(message, opts) + header = { 'Content-Type' => 'application/json' } + response = Gitlab::HTTP.post(webhook, headers: header, body: { text: message.summary }.to_json) + + response if response.success? + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index eb7cfe9a2fc..16b3e1c09c5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -216,6 +216,7 @@ class Project < ApplicationRecord has_one :pipelines_email_integration, class_name: 'Integrations::PipelinesEmail' has_one :pivotaltracker_integration, class_name: 'Integrations::Pivotaltracker' has_one :prometheus_integration, class_name: 'Integrations::Prometheus', inverse_of: :project + has_one :pumble_integration, class_name: 'Integrations::Pumble' has_one :pushover_integration, class_name: 'Integrations::Pushover' has_one :redmine_integration, class_name: 'Integrations::Redmine' has_one :shimo_integration, class_name: 'Integrations::Shimo' diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index bd6831ff3b2..6ca5aaf061e 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -7,12 +7,13 @@ - return unless branches.any? -.card - .card-header += render Pajamas::CardComponent.new(card_options: {class: 'gl-mb-5'}, body_options: {class: 'gl-py-0'}, footer_options: {class: 'gl-text-center'}) do |c| + - c.header do = panel_title - %ul.content-list.all-branches.qa-all-branches - - branches.first(overview_max_branches).each do |branch| - = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any? + - c.body do + %ul.content-list.all-branches.qa-all-branches + - branches.first(overview_max_branches).each do |branch| + = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any? - if branches.size > overview_max_branches - .card-footer.text-center + - c.footer do = link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state } diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 00d518450e9..f607a21ad21 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -7,7 +7,7 @@ - else = sprite_icon('star-o', css_class: 'icon') %span= s_('ProjectOverview|Star') - = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm star-count count' do + = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do = @project.star_count - else @@ -15,5 +15,5 @@ = link_to new_user_session_path, class: 'gl-button btn btn-default btn-sm has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do = sprite_icon('star-o', css_class: 'icon') %span= s_('ProjectOverview|Star') - = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm star-count count' do + = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do = @project.star_count diff --git a/config/feature_flags/development/update_vuln_identifiers_flag.yml b/config/feature_flags/development/update_vuln_identifiers_flag.yml index 62fdc08ce34..3e0e7f4fa41 100644 --- a/config/feature_flags/development/update_vuln_identifiers_flag.yml +++ b/config/feature_flags/development/update_vuln_identifiers_flag.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362179 milestone: '15.1' type: development group: group::static analysis -default_enabled: false +default_enabled: true diff --git a/config/metrics/counts_all/20220802141715_groups_inheriting_pumble_active.yml b/config/metrics/counts_all/20220802141715_groups_inheriting_pumble_active.yml new file mode 100644 index 00000000000..960f859b07f --- /dev/null +++ b/config/metrics/counts_all/20220802141715_groups_inheriting_pumble_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.groups_inheriting_pumble_active +description: Count of active groups inheriting integrations for Pumble +product_section: dev +product_stage: ecosystem +product_group: integrations +product_category: integrations +value_type: number +status: active +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +performance_indicator_type: [] +milestone: "15.3" diff --git a/config/metrics/counts_all/20220802141715_groups_pumble_active.yml b/config/metrics/counts_all/20220802141715_groups_pumble_active.yml new file mode 100644 index 00000000000..3a62a002f10 --- /dev/null +++ b/config/metrics/counts_all/20220802141715_groups_pumble_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.groups_pumble_active +description: Count of groups with active integrations for Pumble +product_section: dev +product_stage: ecosystem +product_group: integrations +product_category: integrations +value_type: number +status: active +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +performance_indicator_type: [] +milestone: "15.3" diff --git a/config/metrics/counts_all/20220802141715_instances_pumble_active.yml b/config/metrics/counts_all/20220802141715_instances_pumble_active.yml new file mode 100644 index 00000000000..d313ee39b31 --- /dev/null +++ b/config/metrics/counts_all/20220802141715_instances_pumble_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.instances_pumble_active +description: Count of active instance-level integrations for Pumble +product_section: dev +product_stage: ecosystem +product_group: integrations +product_category: integrations +value_type: number +status: active +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +performance_indicator_type: [] +milestone: "15.3" diff --git a/config/metrics/counts_all/20220802141715_projects_inheriting_pumble_active.yml b/config/metrics/counts_all/20220802141715_projects_inheriting_pumble_active.yml new file mode 100644 index 00000000000..ae928de6140 --- /dev/null +++ b/config/metrics/counts_all/20220802141715_projects_inheriting_pumble_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.projects_inheriting_pumble_active +description: Count of active projects inheriting integrations for Pumble +product_section: dev +product_stage: ecosystem +product_group: integrations +product_category: integrations +value_type: number +status: active +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +performance_indicator_type: [] +milestone: "15.3" diff --git a/config/metrics/counts_all/20220802141715_projects_pumble_active.yml b/config/metrics/counts_all/20220802141715_projects_pumble_active.yml new file mode 100644 index 00000000000..5f352a1cae4 --- /dev/null +++ b/config/metrics/counts_all/20220802141715_projects_pumble_active.yml @@ -0,0 +1,21 @@ +--- +data_category: optional +key_path: counts.projects_pumble_active +description: Count of projects with active integrations for Pumble +product_section: dev +product_stage: ecosystem +product_group: integrations +product_category: integrations +value_type: number +status: active +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate +performance_indicator_type: [] +milestone: "15.3" diff --git a/db/docs/integrations.yml b/db/docs/integrations.yml index 4629b7558ed..5100ee77fa7 100644 --- a/db/docs/integrations.yml +++ b/db/docs/integrations.yml @@ -38,6 +38,7 @@ classes: - Integrations::PipelinesEmail - Integrations::Pivotaltracker - Integrations::Prometheus +- Integrations::Pumble - Integrations::Pushover - Integrations::Redmine - Integrations::Shimo diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md index 06eb9ffc84e..99876cdf503 100644 --- a/doc/administration/external_pipeline_validation.md +++ b/doc/administration/external_pipeline_validation.md @@ -46,6 +46,7 @@ required number of seconds. "user", "pipeline", "builds", + "total_builds_count", "namespace" ], "properties" : { @@ -61,7 +62,9 @@ required number of seconds. "properties": { "id": { "type": "integer" }, "path": { "type": "string" }, - "created_at": { "type": ["string", "null"], "format": "date-time" } + "created_at": { "type": ["string", "null"], "format": "date-time" }, + "shared_runners_enabled": { "type": "boolean" }, + "group_runners_enabled": { "type": "boolean" } } }, "user": { @@ -121,6 +124,7 @@ required number of seconds. } } }, + "total_builds_count": { "type": "integer" }, "namespace": { "type": "object", "required": [ diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 974676315ad..dcc84288fd0 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -1077,6 +1077,9 @@ License.current.trial? # License ID for lookup on CustomersDot License.current.license_id + +# License data in Base64-encoded ASCII format +License.current.data ``` ### Check if a project feature is available on the instance diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 570f35839e1..019546702b2 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -20380,6 +20380,7 @@ State of a Sentry error. | `PIPELINES_EMAIL_SERVICE` | PipelinesEmailService type. | | `PIVOTALTRACKER_SERVICE` | PivotaltrackerService type. | | `PROMETHEUS_SERVICE` | PrometheusService type. | +| `PUMBLE_SERVICE` | PumbleService type. | | `PUSHOVER_SERVICE` | PushoverService type. | | `REDMINE_SERVICE` | RedmineService type. | | `SHIMO_SERVICE` | ShimoService type. | @@ -20719,8 +20720,6 @@ Vulnerability sort values. | ----- | ----------- | | `detected_asc` | Detection timestamp in ascending order. | | `detected_desc` | Detection timestamp in descending order. | -| `report_type_asc` | Report Type in ascending order. | -| `report_type_desc` | Report Type in descending order. | | `severity_asc` | Severity in ascending order. | | `severity_desc` | Severity in descending order. | diff --git a/doc/api/integrations.md b/doc/api/integrations.md index 28f1629c938..13ec7c6099d 100644 --- a/doc/api/integrations.md +++ b/doc/api/integrations.md @@ -383,6 +383,51 @@ Get Unify Circuit integration settings for a project. GET /projects/:id/integrations/unify-circuit ``` +## Pumble + +Pumble chat tool. + +### Create/Edit Pumble integration + +Set Pumble integration for a project. + +```plaintext +PUT /projects/:id/integrations/pumble +``` + +Parameters: + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhook` | string | true | The Pumble webhook. For example, `https://api.pumble.com/workspaces/x/...`. | +| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default is `default`. | +| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events. | +| `confidential_note_events` | boolean | false | Enable notifications for confidential note events. | +| `issues_events` | boolean | false | Enable notifications for issue events. | +| `merge_requests_events` | boolean | false | Enable notifications for merge request events. | +| `note_events` | boolean | false | Enable notifications for note events. | +| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines. | +| `pipeline_events` | boolean | false | Enable notifications for pipeline events. | +| `push_events` | boolean | false | Enable notifications for push events. | +| `tag_push_events` | boolean | false | Enable notifications for tag push events. | +| `wiki_page_events` | boolean | false | Enable notifications for wiki page events. | + +### Disable Pumble integration + +Disable the Pumble integration for a project. Integration settings are preserved. + +```plaintext +DELETE /projects/:id/integrations/pumble +``` + +### Get Pumble integration settings + +Get Pumble integration settings for a project. + +```plaintext +GET /projects/:id/integrations/pumble +``` + ## Webex Teams Webex Teams collaboration tool. diff --git a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md index 60b20c50696..868dae4fc6c 100644 --- a/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md +++ b/doc/architecture/blueprints/ci_data_decay/pipeline_partitioning.md @@ -306,9 +306,19 @@ We also need to build a proof of concept for removing data on the PostgreSQL side (using foreign keys with `ON DELETE CASCADE`) and removing data through Rails associations, as this might be an important area of uncertainty. -We need to [better understand](https://gitlab.com/gitlab-org/gitlab/-/issues/360148) -how unique constraints we are currently using will perform when using the -partitioned schema. +We [learned](https://gitlab.com/gitlab-org/gitlab/-/issues/360148) that `PostgreSQL` +does not allow to create a single index (unique or otherwise) across all partitions of a table. + +One solution to solve this problem is to embed the partitioning key inside the uniqueness constraint. + +This might mean prepending the partition ID in a hexadecimal format before the token itself and storing +the concatenated string in a database. To do that we would need to reserve an appropriate number of +leading bytes in a token to accommodate for the maximum number of partitions we may have in the future. +It seems that reserving four characters, what would translate into 16-bits number in base-16, +might be sufficient. The maximum number we can encode this way would be FFFF, what is 65535 in decimal. + +This would provide a unique constraint per-partition which +is sufficient for global uniqueness. We have also designed a query analyzer that makes it possible to detect direct usage of zero partitions, legacy tables that have been attached as first diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md index 7e9d35d9c98..0ebc58dd669 100644 --- a/doc/development/service_ping/implement.md +++ b/doc/development/service_ping/implement.md @@ -269,9 +269,15 @@ Arguments: #### Ordinary Redis counters -Example of implementation: +Example of implementation: [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb), using Redis methods [`INCR`](https://redis.io/commands/incr) and [`GET`](https://redis.io/commands/get). -Using Redis methods [`INCR`](https://redis.io/commands/incr/), [`GET`](https://redis.io/commands/get/), and [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb) +Events are handled by counter classes in the `Gitlab::UsageDataCounters` namespace, inheriting from `BaseCounter`, that are either: + +1. Listed in [`Gitlab::UsageDataCounters::COUNTERS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters.rb#L5) to be then included in `Gitlab::UsageData`. + +1. Specified in the metric definition using the `RedisMetric` instrumentation class as a `counter_class` option to be picked up using the [metric instrumentation](metrics_instrumentation.md) framework. Refer to the [Redis metrics](metrics_instrumentation.md#redis-metrics) documentation for an example implementation. + +Inheriting classes are expected to override `KNOWN_EVENTS` and `PREFIX` constants to build event names and associated metrics. For example, for prefix `issues` and events array `%w[create, update, delete]`, three metrics will be added to the Service Ping payload: `counts.issues_create`, `counts.issues_update` and `counts.issues_delete`. ##### `UsageData` API diff --git a/doc/user/analytics/img/product_analytics_commits_per_mr_v14_4.png b/doc/user/analytics/img/product_analytics_commits_per_mr_v14_4.png deleted file mode 100644 index 2bfde7beead..00000000000 Binary files a/doc/user/analytics/img/product_analytics_commits_per_mr_v14_4.png and /dev/null differ diff --git a/doc/user/analytics/img/productivity_analytics_time_to_merge_v14_4.png b/doc/user/analytics/img/productivity_analytics_time_to_merge_v14_4.png deleted file mode 100644 index 0b30aff2c7a..00000000000 Binary files a/doc/user/analytics/img/productivity_analytics_time_to_merge_v14_4.png and /dev/null differ diff --git a/doc/user/analytics/img/productivity_analytics_trendline_v14_4.png b/doc/user/analytics/img/productivity_analytics_trendline_v14_4.png deleted file mode 100644 index e0b3c54dee2..00000000000 Binary files a/doc/user/analytics/img/productivity_analytics_trendline_v14_4.png and /dev/null differ diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md index be710f8cbd7..f8b28ead155 100644 --- a/doc/user/analytics/productivity_analytics.md +++ b/doc/user/analytics/productivity_analytics.md @@ -4,103 +4,65 @@ group: Optimize info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Productivity Analytics **(PREMIUM)** +# Productivity analytics **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12079) in GitLab 12.3. +You can use productivity analytics to identify: -Track development velocity with Productivity Analytics. +- Your development velocity based on how long it takes for a merge request to merge. +- The most time consuming merge requests and potential causes. +- Authors, labels, or milestones with the longest time to merge, or most changes. -For many companies, the development cycle is a black box and getting an estimate of how -long, on average, it takes to deliver features is an enormous endeavor. +Use productivity analytics to view the following merge request statistics for your groups: -While [Value Stream Analytics](../analytics/value_stream_analytics.md) focuses on the entire -Software Development Life Cycle (SDLC) process, Productivity Analytics provides a way for Engineering Management to drill down in a systematic way to uncover patterns and causes for success or failure at an individual, project, or group level. +- Amount of time between merge request creation and merge. +- Amount of time between commits, comments, and merge. +- Complexity of changes, like number of lines of code per commit and number of files. -Productivity can slow down for many reasons ranging from degrading codebase to quickly growing teams. To investigate, department or team leaders can start by visualizing the time it takes for merge requests to be merged. +To view merge request data for projects, use [Merge request analytics](../analytics/merge_request_analytics.md). -## Visualizations and metrics +## View productivity analytics -With Productivity Analytics, GitLab users can: +Prerequisite: -- Visualize typical merge request (MR) lifetime and statistics. A histogram shows the distribution of the time elapsed between creating and merging merge requests. -- Drill down into the most time consuming merge requests, select outliers, and filter subsequent charts to investigate potential causes. -- Filter by group, project, author, label, milestone, or a specific date range. For example, filter down to the merge requests of a specific author in a group or project during a milestone or specific date range. -- Measure velocity over time. To observe progress, visualize the trends of each metric from the charts over time. Zoom in on a particular date range if you notice outliers. +- You must have at least the Reporter role for the group. -## Metrics charts +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Analytics > Productivity**. +1. Optional. Filter results: + 1. Select a project from the dropdown list. + 1. To filter results by author, milestone, or label, + select **Filter results...** and enter a value. + 1. To adjust the date range: + - In the **From** field, select a start date. + - In the **To** field, select an end date. -To access the charts, navigate to a group's sidebar and select **Analytics > Productivity Analytics**. -Metrics and visualizations of **merged** merge requests are available on a project or group level. +## View time metrics for merge requests -### Time to merge +Use the following charts in productivity analytics to view the velocity of your merge requests: -The **Time to merge** histogram shows the number of merge requests and the number -of days it took to merge after creation. Select a column to filter subsequent charts. +- **Time to merge**: number of days it took for a +merge requests to merge after they were created. +- **Trendline**: number of merge requests that were merged in a specific time period. -![Metrics for number of days merge requests per number of days](img/productivity_analytics_time_to_merge_v14_4.png) +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Analytics > Productivity**. -### Trendline +To filter time metrics: -The **Trendline** scatterplot shows all merge requests on a certain date, -and the days it took to complete the action and a 30 day rolling median. Select the dropdown to view: +1. To filter the **Trendline** chart, in the **Time to merge** chart, select a column. +1. To view a specific merge request, below the charts, select a merge request from the **List**. -- Time from first commit to first comment. -- Time from first comment until last commit. -- Time from last commit to merge. -- Number of commits per merge request. -- Number of lines of code (LOC) per commit. -- Number of files touched. +## View commit statistics -![Metrics for amount of merge requests merged on a certain date](img/productivity_analytics_trendline_v14_4.png) +To view commit statistics for your group: -### Commits and merge request size +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Analytics > Productivity**. +1. Under the **Trendline** scatterplot, view the commit statistics: + - The left histogram shows the number of hours between commits, comments, and merges. + - The right histogram shows the number of commits and changes per merge request. -Under the **Trendline** scatterplot, the left-side histogram shows -the time taken (in hours) between commits and comments until the merge -request is merged. Select the dropdown to view: +To filter commit statistics: -- Time from first commit to first comment. -- Time from first comment until last commit. -- Time from last commit to merge. - -The right-side histogram shows the size or complexity of a merge request. -Select the dropdown to view: - -- Number of commits per merge request. -- Number of lines of code (LOC) per commit. -- Number of files touched. - -![Metrics for amount of commits and complexity of changes per merge request.](img/product_analytics_commits_per_mr_v14_4.png) - -### Merge request list - -The **List** table shows a list of merge requests with their respective time duration metrics. - -Sort metrics by: - -- Time from first commit to first comment. -- Time from first comment until last commit. -- Time from last commit to merge. - -Filter metrics by: - -- Number of commits per merge request. -- Number of lines of code per commit. -- Number of files touched. - -## Filter by date range - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13188) in GitLab 12.4. - -You can filter analytics based on a date range. To filter results: - -1. Select a group. -1. Optional. Select a project. -1. Select a date range by using the available date pickers. - -## Permissions - -The **Productivity Analytics** dashboard can be accessed only: - -- On [GitLab Premium](https://about.gitlab.com/pricing/) and above. -- By users with at least the Reporter role. +1. To view different types of commit data, select the dropdown list next to each histogram. +1. To view a specific merge request, below the charts, select a merge request from the **List**. diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 9200cafab2e..7c7d5380a24 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -149,7 +149,7 @@ base address for Docker images. You can override this for most scanners by setti The [Container Scanning](container_scanning/index.md) analyzer is an exception, and it does not use the `SECURE_ANALYZERS_PREFIX` variable. To override its Docker image, see -the instructions for +the instructions for [Running container scanning in an offline environment](container_scanning/index.md#running-container-scanning-in-an-offline-environment). ## Default behavior of GitLab security scanning tools @@ -390,8 +390,10 @@ Validation depends on the schema version declared in the security report artifac - If your security report specifies a supported schema version, GitLab uses this version to validate. - If your security report uses a deprecated version, GitLab attempts validation against that version and adds a deprecation warning to the validation result. -- If your security report uses a version that is not supported, GitLab attempts to validate it against the latest schema version available in GitLab. -- If your security report does not specify a schema version, GitLab attempts to validate it against the lastest schema version available in GitLab. Since the `version` property is required, validation always fails in this case, but other validation errors may also be present. +- If your security report uses a supported MAJOR-MINOR version of the report schema but the PATCH version doesn't match any vendored versions, GitLab attempts to validate it against latest vendored PATCH version of the schema. + - Example: security report uses version 14.1.1 but the latest vendored version is 14.1.0. GitLab would validate against schema version 14.1.0. +- If your security report uses a version that is not supported, GitLab attempts to validate it against the latest schema version available in your installation but doesn't ingest the report. +- If your security report does not specify a schema version, GitLab attempts to validate it against the latest schema version available in GitLab. Because the `version` property is required, validation always fails in this case, but other validation errors may also be present. You can always find supported and deprecated schema versions in the [source code](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/parsers/security/validators/schema_validator.rb). diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index 7af2e431157..3ef40fdfe44 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -73,6 +73,7 @@ You can configure the following integrations. | [Pipelines emails](pipeline_status_emails.md) | Send the pipeline status to a list of recipients by email. | **{dotted-circle}** No | | [Pivotal Tracker](pivotal_tracker.md) | Add commit messages as comments to Pivotal Tracker stories. | **{dotted-circle}** No | | [Prometheus](prometheus.md) | Monitor application metrics. | **{dotted-circle}** No | +| [Pumble](pumble.md) | Send event notifications to a Pumble channel. | **{dotted-circle}** No | | Pushover | Get real-time notifications on your device. | **{dotted-circle}** No | | [Redmine](redmine.md) | Use Redmine as the issue tracker. | **{dotted-circle}** No | | [Slack application](gitlab_slack_application.md) | Use Slack's official GitLab application. | **{dotted-circle}** No | diff --git a/doc/user/project/integrations/pumble.md b/doc/user/project/integrations/pumble.md new file mode 100644 index 00000000000..cd28a7c0048 --- /dev/null +++ b/doc/user/project/integrations/pumble.md @@ -0,0 +1,39 @@ +--- +stage: Ecosystem +group: Integrations +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Pumble **(FREE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93623) in GitLab 15.3. + +You can configure GitLab to send notifications to a Pumble channel: + +1. Create a webhook for the channel. +1. Add the webhook to GitLab. + +## Create a webhook for your Pumble channel + +1. Follow the steps in [Incoming Webhooks for Pumble](https://pumble.com/help/integrations/custom-apps/incoming-webhooks-for-pumble/) in the Pumble documentation. +1. Copy the webhook URL. + +## Configure settings in GitLab + +After you have a webhook URL for your Pumble channel, configure GitLab to send +notifications: + +1. To enable the integration for your group or project: + 1. In your group or project, on the left sidebar, select **Settings > Integrations**. +1. To enable the integration for your instance: + 1. On the top bar, select **Menu > Admin**. + 1. On the left sidebar, select **Settings > Integrations**. +1. Select the **Pumble** integration. +1. Ensure that the **Active** toggle is enabled. +1. Select the checkboxes corresponding to the GitLab events you want to receive in Pumble. +1. Paste the **Webhook** URL for the Pumble channel. +1. Configure the remaining options. +1. Optional. To test the integration, select **Test settings**. +1. Select **Save changes**. + +The Pumble channel begins to receive all applicable GitLab events. diff --git a/doc/user/project/merge_requests/approvals/rules.md b/doc/user/project/merge_requests/approvals/rules.md index 89d75aa196a..72c59803a80 100644 --- a/doc/user/project/merge_requests/approvals/rules.md +++ b/doc/user/project/merge_requests/approvals/rules.md @@ -127,7 +127,7 @@ users were not explicitly listed in the approval rules. ### Group approvers You can add a group of users as approvers, but those users count as approvers only if -they have direct membership to the group. Group approvers are +they have **direct membership** to the group. Inherited members do not count. Group approvers are restricted to only groups [with share access to the project](../../members/share_project_with_groups.md). A user's membership in an approvers group affects their individual ability to diff --git a/glfm_specification/example_snapshots/html.yml b/glfm_specification/example_snapshots/html.yml index f89f03ecc31..8a54ee6f360 100644 --- a/glfm_specification/example_snapshots/html.yml +++ b/glfm_specification/example_snapshots/html.yml @@ -1682,7 +1682,7 @@ static: |-
*foo*
wysiwyg: |- -

*foo*

+

*foo*

04_06__leaf_blocks__html_blocks__013: canonical: |
@@ -1723,7 +1723,7 @@ *bar* wysiwyg: |- -

+

*bar*

04_06__leaf_blocks__html_blocks__016: @@ -2065,7 +2065,7 @@ baz

wysiwyg: |-

Foo - + baz

04_06__leaf_blocks__html_blocks__040: canonical: | @@ -2145,7 +2145,7 @@

foo

wysiwyg: |-
[foo]: /url "title"
-

foo

+

foo

04_07__leaf_blocks__link_reference_definitions__002: canonical: |

foo

@@ -2153,7 +2153,7 @@

foo

wysiwyg: |-
[foo]: /url "the title"
-

foo

+

foo

04_07__leaf_blocks__link_reference_definitions__003: canonical: |

Foo*bar]

@@ -2161,7 +2161,7 @@

Foo*bar]

wysiwyg: |-
[foo*bar\]]: my_(url) "title (with parens)"
-

Foo*bar]

+

Foo*bar]

04_07__leaf_blocks__link_reference_definitions__004: canonical: |

Foo bar

@@ -2169,7 +2169,7 @@

Foo bar

wysiwyg: |-
[foo bar]: my url "title"
-

Foo bar

+

Foo bar

04_07__leaf_blocks__link_reference_definitions__005: canonical: |

-

foo

wysiwyg: |-
[foo]: /url
-

foo

+

foo

04_07__leaf_blocks__link_reference_definitions__008: canonical: |

[foo]:

@@ -2250,14 +2250,14 @@

foo

wysiwyg: |-
[foo]: /url\bar*baz "foo"bar\baz"
-

foo

+

foo

04_07__leaf_blocks__link_reference_definitions__012: canonical: |

foo

static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: url
04_07__leaf_blocks__link_reference_definitions__013: canonical: | @@ -2265,7 +2265,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: first
[foo]: second
04_07__leaf_blocks__link_reference_definitions__014: @@ -2275,7 +2275,7 @@

Foo

wysiwyg: |-
[foo]: /url
-

Foo

+

Foo

04_07__leaf_blocks__link_reference_definitions__015: canonical: |

αγω

@@ -2283,7 +2283,7 @@

αγω

wysiwyg: |-
[αγω]: /φου
-

αγω

+

αγω

04_07__leaf_blocks__link_reference_definitions__016: canonical: "" static: "" @@ -2367,7 +2367,7 @@

bar

wysiwyg: |- -

Foo

+

Foo

[foo]: /url

bar

04_07__leaf_blocks__link_reference_definitions__024: @@ -2381,7 +2381,7 @@ wysiwyg: |-
[foo]: /url

bar

-

foo

+

foo

04_07__leaf_blocks__link_reference_definitions__025: canonical: |

=== @@ -2392,7 +2392,7 @@ wysiwyg: |-

[foo]: /url

=== - foo

+ foo

04_07__leaf_blocks__link_reference_definitions__026: canonical: |

foo, @@ -2406,9 +2406,9 @@

[foo]: /foo-url "foo"
[bar]: /bar-url "bar"
[baz]: /baz-url
-

foo, - bar, - baz

+

foo, + bar, + baz

04_07__leaf_blocks__link_reference_definitions__027: canonical: |

foo

@@ -2419,7 +2419,7 @@
wysiwyg: |- -

foo

+

foo

[foo]: /url
04_07__leaf_blocks__link_reference_definitions__028: canonical: "" @@ -4883,7 +4883,7 @@ static: |-

http://example.com?find=\*

wysiwyg: |- -

http://example.com?find=\*

+

http://example.com?find=\*

06_02__inlines__backslash_escapes__010: canonical: | @@ -4897,14 +4897,14 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

06_02__inlines__backslash_escapes__012: canonical: |

foo

static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /bar* "ti*tle"
06_02__inlines__backslash_escapes__013: canonical: | @@ -4987,14 +4987,14 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

06_03__inlines__entity_and_numeric_character_references__009: canonical: |

foo

static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /föö "föö"
06_03__inlines__entity_and_numeric_character_references__010: canonical: | @@ -5193,7 +5193,7 @@ static: |-

`

wysiwyg: |- -

`

+

`

06_04__inlines__code_spans__018: canonical: |

<http://foo.bar.baz>`

@@ -5207,7 +5207,7 @@ static: |-

http://foo.bar.`baz`

wysiwyg: |- -

http://foo.bar.`baz`

+

http://foo.bar.`baz`

06_04__inlines__code_spans__020: canonical: |

```foo``

@@ -5615,7 +5615,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__055: canonical: |

foo @@ -5723,7 +5723,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__070: canonical: |

** is not an empty emphasis

@@ -5744,7 +5744,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__073: canonical: |

foo @@ -5827,7 +5827,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__084: canonical: |

__ is not an empty emphasis

@@ -6107,21 +6107,21 @@ static: |-

*bar*

wysiwyg: |- -

*bar*

+

*bar*

06_05__inlines__emphasis_and_strong_emphasis__124: canonical: |

_foo bar_

static: |-

_foo bar_

wysiwyg: |- -

_foo bar_

+

_foo bar_

06_05__inlines__emphasis_and_strong_emphasis__125: canonical: |

*

static: |-

*

wysiwyg: |- -

*

+

*

06_05__inlines__emphasis_and_strong_emphasis__126: canonical: |

**

@@ -6187,14 +6187,14 @@ static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__002: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__003: canonical: |

link

@@ -6222,7 +6222,7 @@ static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__007: canonical: |

[link](foo @@ -6247,7 +6247,7 @@ static: |-

a

wysiwyg: |- -

a

+

a

06_07__inlines__links__010: canonical: |

[link](<foo>)

@@ -6274,35 +6274,35 @@ static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__013: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__014: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__015: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__016: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__017: canonical: |

link

@@ -6313,30 +6313,30 @@

link

link

wysiwyg: |- -

link

-

link

-

link

+

link

+

link

+

link

06_07__inlines__links__018: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__019: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__020: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__021: canonical: |

link @@ -6347,21 +6347,21 @@ link link

wysiwyg: |- -

linklinklink

+

linklinklink

06_07__inlines__links__022: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__023: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__024: canonical: |

[link](/url "title "and" title")

@@ -6375,14 +6375,14 @@ static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__026: canonical: |

link

static: |-

link

wysiwyg: |- -

link

+

link

06_07__inlines__links__027: canonical: |

[link] (/uri)

@@ -6396,7 +6396,7 @@ static: |-

link [foo [bar]]

wysiwyg: |- -

link [foo [bar]]

+

link [foo [bar]]

06_07__inlines__links__029: canonical: |

[link] bar](/uri)

@@ -6410,63 +6410,63 @@ static: |-

[link bar

wysiwyg: |- -

[link bar

+

[link bar

06_07__inlines__links__031: canonical: |

link [bar

static: |-

link [bar

wysiwyg: |- -

link [bar

+

link [bar

06_07__inlines__links__032: canonical: |

link foo bar #

static: |-

link foo bar #

wysiwyg: |- -

link foo bar#

+

link foo bar#

06_07__inlines__links__033: canonical: |

moon

static: |-

moon

wysiwyg: |- -

moon

+

moon

06_07__inlines__links__034: canonical: |

[foo bar](/uri)

static: |-

[foo bar](/uri)

wysiwyg: |- -

[foo bar](/uri)

+

[foo bar](/uri)

06_07__inlines__links__035: canonical: |

[foo [bar baz](/uri)](/uri)

static: |-

[foo [bar baz](/uri)](/uri)

wysiwyg: |- -

[foo [bar baz](/uri)](/uri)

+

[foo [bar baz](/uri)](/uri)

06_07__inlines__links__036: canonical: |

[foo](uri2)

static: |-

[foo](uri2)

wysiwyg: |- -

[foo](uri2)

+

[foo](uri2)

06_07__inlines__links__037: canonical: |

*foo*

static: |-

*foo*

wysiwyg: |- -

*foo*

+

*foo*

06_07__inlines__links__038: canonical: |

foo *bar

static: |-

foo *bar

wysiwyg: |- -

foo *bar

+

foo *bar

06_07__inlines__links__039: canonical: |

foo [bar baz]

@@ -6501,7 +6501,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[bar]: /url "title"
06_07__inlines__links__044: canonical: | @@ -6509,7 +6509,7 @@ static: |-

link [foo [bar]]

wysiwyg: |- -

link [foo [bar]]

+

link [foo [bar]]

[ref]: /uri
06_07__inlines__links__045: canonical: | @@ -6517,7 +6517,7 @@ static: |-

link [bar

wysiwyg: |- -

link [bar

+

link [bar

[ref]: /uri
06_07__inlines__links__046: canonical: | @@ -6525,7 +6525,7 @@ static: |-

link foo bar #

wysiwyg: |- -

link foo bar#

+

link foo bar#

[ref]: /uri
06_07__inlines__links__047: canonical: | @@ -6533,7 +6533,7 @@ static: |-

moon

wysiwyg: |- -

moon

+

moon

[ref]: /uri
06_07__inlines__links__048: canonical: | @@ -6541,7 +6541,7 @@ static: |-

[foo bar]ref

wysiwyg: |- -

[foo bar]ref

+

[foo bar]ref

[ref]: /uri
06_07__inlines__links__049: canonical: | @@ -6549,7 +6549,7 @@ static: |-

[foo bar baz]ref

wysiwyg: |- -

[foo bar baz]ref

+

[foo bar baz]ref

[ref]: /uri
06_07__inlines__links__050: canonical: | @@ -6557,7 +6557,7 @@ static: |-

*foo*

wysiwyg: |- -

*foo*

+

*foo*

[ref]: /uri
06_07__inlines__links__051: canonical: | @@ -6565,7 +6565,7 @@ static: |-

foo *bar

wysiwyg: |- -

foo *bar

+

foo *bar

[ref]: /uri
06_07__inlines__links__052: canonical: | @@ -6597,7 +6597,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[bar]: /url "title"
06_07__inlines__links__056: canonical: | @@ -6605,7 +6605,7 @@ static: |-

Толпой is a Russian word.

wysiwyg: |- -

Толпой is a Russian word.

+

Толпой is a Russian word.

[толпой]: /url
06_07__inlines__links__057: canonical: | @@ -6614,14 +6614,14 @@

Baz

wysiwyg: |-
[foo bar]: /url
-

Baz

+

Baz

06_07__inlines__links__058: canonical: |

[foo] bar

static: |-

[foo] bar

wysiwyg: |- -

[foo] bar

+

[foo] bar

[bar]: /url "title"
06_07__inlines__links__059: canonical: | @@ -6632,7 +6632,7 @@ bar

wysiwyg: |-

[foo] - bar

+ bar

[bar]: /url "title"
06_07__inlines__links__060: canonical: | @@ -6642,7 +6642,7 @@ wysiwyg: |-
[foo]: /url1
[foo]: /url2
-

bar

+

bar

06_07__inlines__links__061: canonical: |

[bar][foo!]

@@ -6687,7 +6687,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[ref\[]: /uri
06_07__inlines__links__066: canonical: | @@ -6696,7 +6696,7 @@

bar\

wysiwyg: |-
[bar\\]: /uri
-

bar\

+

bar\

06_07__inlines__links__067: canonical: |

[]

@@ -6729,7 +6729,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /url "title"
06_07__inlines__links__070: canonical: | @@ -6737,7 +6737,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[*foo* bar]: /url "title"
06_07__inlines__links__071: canonical: | @@ -6745,7 +6745,7 @@ static: |-

Foo

wysiwyg: |- -

Foo

+

Foo

[foo]: /url "title"
06_07__inlines__links__072: canonical: | @@ -6755,7 +6755,7 @@

foo []

wysiwyg: |- -

foo +

foo []

[foo]: /url "title"
06_07__inlines__links__073: @@ -6764,7 +6764,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /url "title"
06_07__inlines__links__074: canonical: | @@ -6772,7 +6772,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[*foo* bar]: /url "title"
06_07__inlines__links__075: canonical: | @@ -6780,7 +6780,7 @@ static: |-

[foo bar]

wysiwyg: |- -

[foo bar]

+

[foo bar]

[*foo* bar]: /url "title"
06_07__inlines__links__076: canonical: | @@ -6788,7 +6788,7 @@ static: |-

[[bar foo

wysiwyg: |- -

[[bar foo

+

[[bar foo

[foo]: /url
06_07__inlines__links__077: canonical: | @@ -6796,7 +6796,7 @@ static: |-

Foo

wysiwyg: |- -

Foo

+

Foo

[foo]: /url "title"
06_07__inlines__links__078: canonical: | @@ -6804,7 +6804,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[foo]: /url
06_07__inlines__links__079: canonical: | @@ -6821,14 +6821,14 @@

*foo*

wysiwyg: |-
[foo*]: /url
-

*foo*

+

*foo*

06_07__inlines__links__081: canonical: |

foo

static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /url1
[bar]: /url2
06_07__inlines__links__082: @@ -6837,7 +6837,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /url1
06_07__inlines__links__083: canonical: | @@ -6853,7 +6853,7 @@ static: |-

foo(not a link)

wysiwyg: |- -

foo(not a link)

+

foo(not a link)

[foo]: /url1
06_07__inlines__links__085: canonical: | @@ -6861,7 +6861,7 @@ static: |-

[foo]bar

wysiwyg: |- -

[foo]bar

+

[foo]bar

[baz]: /url
06_07__inlines__links__086: canonical: | @@ -6869,7 +6869,7 @@ static: |-

foobaz

wysiwyg: |- -

foobaz

+

foobaz

[baz]: /url1
[bar]: /url2
06_07__inlines__links__087: @@ -6878,7 +6878,7 @@ static: |-

[foo]bar

wysiwyg: |- -

[foo]bar

+

[foo]bar

[baz]: /url1
[foo]: /url2
06_08__inlines__images__001: @@ -6887,14 +6887,14 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

06_08__inlines__images__002: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[foo *bar*]: train.jpg "train & tracks"
06_08__inlines__images__003: canonical: | @@ -6902,21 +6902,21 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_08__inlines__images__004: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_08__inlines__images__005: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[foo *bar*]: train.jpg "train & tracks"
06_08__inlines__images__006: canonical: | @@ -6924,7 +6924,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[foobar]: train.jpg "train & tracks"
06_08__inlines__images__007: canonical: | @@ -6932,35 +6932,35 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

06_08__inlines__images__008: canonical: |

My foo bar

static: |-

My foo bar

wysiwyg: |- -

My foo bar

+

My foo bar

06_08__inlines__images__009: canonical: |

foo

static: |-

foo

wysiwyg: |- -

foo

+

foo

06_08__inlines__images__010: canonical: |

static: |-

wysiwyg: |- -

+

06_08__inlines__images__011: canonical: |

foo

static: |-

foo

wysiwyg: |- -

foo

+

foo

[bar]: /url
06_08__inlines__images__012: canonical: | @@ -6968,7 +6968,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[bar]: /url
06_08__inlines__images__013: canonical: | @@ -6976,7 +6976,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /url "title"
06_08__inlines__images__014: canonical: | @@ -6984,7 +6984,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[*foo* bar]: /url "title"
06_08__inlines__images__015: canonical: | @@ -6992,7 +6992,7 @@ static: |-

Foo

wysiwyg: |- -

Foo

+

Foo

[foo]: /url "title"
06_08__inlines__images__016: canonical: | @@ -7002,7 +7002,7 @@

foo []

wysiwyg: |- -

foo +

foo []

[foo]: /url "title"
06_08__inlines__images__017: @@ -7011,7 +7011,7 @@ static: |-

foo

wysiwyg: |- -

foo

+

foo

[foo]: /url "title"
06_08__inlines__images__018: canonical: | @@ -7019,7 +7019,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

[*foo* bar]: /url "title"
06_08__inlines__images__019: canonical: | @@ -7037,7 +7037,7 @@ static: |-

Foo

wysiwyg: |- -

Foo

+

Foo

[foo]: /url "title"
06_08__inlines__images__021: canonical: | @@ -7053,7 +7053,7 @@ static: |-

!foo

wysiwyg: |- -

!foo

+

!foo

[foo]: /url "title"
06_09__inlines__autolinks__001: canonical: | @@ -7061,7 +7061,7 @@ static: |-

http://foo.bar.baz

wysiwyg: |- -

http://foo.bar.baz

+

http://foo.bar.baz

06_09__inlines__autolinks__002: canonical: |

http://foo.bar.baz/test?q=hello&id=22&boolean

@@ -7075,28 +7075,28 @@ static: |-

irc://foo.bar:2233/baz

wysiwyg: |- -

irc://foo.bar:2233/baz

+

irc://foo.bar:2233/baz

06_09__inlines__autolinks__004: canonical: |

MAILTO:FOO@BAR.BAZ

static: |-

MAILTO:FOO@BAR.BAZ

wysiwyg: |- -

MAILTO:FOO@BAR.BAZ

+

MAILTO:FOO@BAR.BAZ

06_09__inlines__autolinks__005: canonical: |

a+b+c:d

static: |-

a+b+c:d

wysiwyg: |- -

a+b+c:d

+

a+b+c:d

06_09__inlines__autolinks__006: canonical: |

made-up-scheme://foo,bar

static: |-

made-up-scheme://foo,bar

wysiwyg: |- -

made-up-scheme://foo,bar

+

made-up-scheme://foo,bar

06_09__inlines__autolinks__007: canonical: |

http://../

@@ -7110,7 +7110,7 @@ static: |-

localhost:5001/foo

wysiwyg: |- -

localhost:5001/foo

+

localhost:5001/foo

06_09__inlines__autolinks__009: canonical: |

<http://foo.bar/baz bim>

@@ -7159,7 +7159,7 @@ static: |-

< http://foo.bar >

wysiwyg: |- -

< http://foo.bar >

+

< http://foo.bar >

06_09__inlines__autolinks__016: canonical: |

<m:abc>

@@ -7180,7 +7180,7 @@ static: |-

http://example.com

wysiwyg: |- -

http://example.com

+

http://example.com

06_09__inlines__autolinks__019: canonical: |

foo@bar.example.com

@@ -7194,7 +7194,7 @@ static: |-

www.commonmark.org

wysiwyg: |- -

www.commonmark.org

+

www.commonmark.org

06_10__inlines__autolinks_extension__002: canonical: |

Visit www.commonmark.org/help for more information.

@@ -7210,7 +7210,7 @@

Visit www.commonmark.org.

Visit www.commonmark.org/a.b.

wysiwyg: |- -

Visit www.commonmark.org.

+

Visit www.commonmark.org.

Visit www.commonmark.org/a.b.

06_10__inlines__autolinks_extension__004: canonical: | @@ -7262,7 +7262,7 @@

(Visit https://encrypted.google.com/search?q=Markup+(business))

Anonymous FTP is available at ftp://foo.bar.baz.

wysiwyg: |- -

http://commonmark.org

+

http://commonmark.org

(Visit https://encrypted.google.com/search?q=Markup+(business))

Anonymous FTP is available at ftp://foo.bar.baz.

06_10__inlines__autolinks_extension__009: diff --git a/glfm_specification/example_snapshots/prosemirror_json.yml b/glfm_specification/example_snapshots/prosemirror_json.yml index 73fcef4dbc0..7f08d4e4a1d 100644 --- a/glfm_specification/example_snapshots/prosemirror_json.yml +++ b/glfm_specification/example_snapshots/prosemirror_json.yml @@ -3145,7 +3145,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/bar", + "href": "bar", "target": "_blank", "class": null, "title": null, @@ -3230,7 +3230,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo", + "href": "foo", "target": "_blank", "class": null, "title": null, @@ -3748,7 +3748,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/bar", + "href": "bar", "target": "_blank", "class": null, "title": null, @@ -3906,7 +3906,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -3947,7 +3947,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "the title", @@ -3988,7 +3988,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/my_(url)", + "href": "my_(url)", "target": "_blank", "class": null, "title": "title (with parens)", @@ -4029,7 +4029,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/my%20url", + "href": "my%20url", "target": "_blank", "class": null, "title": "title", @@ -4070,7 +4070,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "\ntitle\nline1\nline2\n", @@ -4144,7 +4144,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -4274,7 +4274,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url%5Cbar*baz", + "href": "/url%5Cbar*baz", "target": "_blank", "class": null, "title": "foo\"bar\\baz", @@ -4301,7 +4301,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "url", "target": "_blank", "class": null, "title": null, @@ -4342,7 +4342,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/first", + "href": "first", "target": "_blank", "class": null, "title": null, @@ -4411,7 +4411,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -4452,7 +4452,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/%CF%86%CE%BF%CF%85", + "href": "/%CF%86%CE%BF%CF%85", "target": "_blank", "class": null, "title": null, @@ -4655,7 +4655,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -4739,7 +4739,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -4784,7 +4784,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -4853,7 +4853,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo-url", + "href": "/foo-url", "target": "_blank", "class": null, "title": "foo", @@ -4873,7 +4873,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/bar-url", + "href": "/bar-url", "target": "_blank", "class": null, "title": "bar", @@ -4893,7 +4893,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/baz-url", + "href": "/baz-url", "target": "_blank", "class": null, "title": null, @@ -4920,7 +4920,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -10683,7 +10683,7 @@ { "type": "link", "attrs": { - "href": "http://example.com/?find=%5C*", + "href": "http://example.com?find=%5C*", "target": "_blank", "class": null, "title": null, @@ -10719,7 +10719,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/bar*", + "href": "/bar*", "target": "_blank", "class": null, "title": "ti*tle", @@ -10746,7 +10746,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/bar*", + "href": "/bar*", "target": "_blank", "class": null, "title": "ti*tle", @@ -10905,7 +10905,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/f%C3%B6%C3%B6", + "href": "/f%C3%B6%C3%B6", "target": "_blank", "class": null, "title": "föö", @@ -10932,7 +10932,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/f%C3%B6%C3%B6", + "href": "/f%C3%B6%C3%B6", "target": "_blank", "class": null, "title": "föö", @@ -11466,7 +11466,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/%60", + "href": "`", "target": "_blank", "class": null, "title": null, @@ -11517,7 +11517,7 @@ { "type": "link", "attrs": { - "href": "http://foo.bar.`baz/", + "href": "http://foo.bar.%60baz", "target": "_blank", "class": null, "title": null, @@ -12673,7 +12673,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -13143,7 +13143,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -13212,7 +13212,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -13571,7 +13571,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -14477,7 +14477,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -14508,7 +14508,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -14536,7 +14536,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/foo", + "src": "foo", "alt": null, "title": "*", "uploading": false, @@ -14764,7 +14764,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": "title", @@ -14791,7 +14791,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -14887,7 +14887,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/my%20uri", + "href": "/my%20uri", "target": "_blank", "class": null, "title": null, @@ -14944,7 +14944,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/b)c", + "href": "b)c", "target": "_blank", "class": null, "title": null, @@ -15010,7 +15010,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/(foo)", + "href": "(foo)", "target": "_blank", "class": null, "title": null, @@ -15037,7 +15037,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo(and(bar))", + "href": "foo(and(bar))", "target": "_blank", "class": null, "title": null, @@ -15064,7 +15064,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo(and(bar)", + "href": "foo(and(bar)", "target": "_blank", "class": null, "title": null, @@ -15091,7 +15091,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo(and(bar)", + "href": "foo(and(bar)", "target": "_blank", "class": null, "title": null, @@ -15118,7 +15118,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo):", + "href": "foo):", "target": "_blank", "class": null, "title": null, @@ -15145,7 +15145,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/#fragment", + "href": "#fragment", "target": "_blank", "class": null, "title": null, @@ -15166,7 +15166,7 @@ { "type": "link", "attrs": { - "href": "http://example.com/#fragment", + "href": "http://example.com#fragment", "target": "_blank", "class": null, "title": null, @@ -15187,7 +15187,7 @@ { "type": "link", "attrs": { - "href": "http://example.com/?foo=3#frag", + "href": "http://example.com?foo=3#frag", "target": "_blank", "class": null, "title": null, @@ -15214,7 +15214,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo%5Cbar", + "href": "foo%5Cbar", "target": "_blank", "class": null, "title": null, @@ -15241,7 +15241,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/foo%20b%C3%A4", + "href": "foo%20b%C3%A4", "target": "_blank", "class": null, "title": null, @@ -15268,7 +15268,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/%22title%22", + "href": "%22title%22", "target": "_blank", "class": null, "title": null, @@ -15295,7 +15295,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -15322,7 +15322,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title \"\"", @@ -15349,7 +15349,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url%C2%A0%22title%22", + "href": "/url%C2%A0%22title%22", "target": "_blank", "class": null, "title": null, @@ -15391,7 +15391,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title \"and\" title", @@ -15418,7 +15418,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": "title", @@ -15460,7 +15460,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15506,7 +15506,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15533,7 +15533,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15560,7 +15560,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15576,7 +15576,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15595,7 +15595,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15617,7 +15617,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15650,7 +15650,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/moon.jpg", + "src": "moon.jpg", "alt": "moon", "title": null, "uploading": false, @@ -15660,7 +15660,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15690,7 +15690,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15734,7 +15734,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15774,7 +15774,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/uri3", + "src": "uri3", "alt": "[foo](uri2)", "title": null, "uploading": false, @@ -15802,7 +15802,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -15829,7 +15829,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/baz*", + "href": "baz*", "target": "_blank", "class": null, "title": null, @@ -15950,7 +15950,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -15991,7 +15991,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16032,7 +16032,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16073,7 +16073,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16089,7 +16089,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16108,7 +16108,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16130,7 +16130,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16177,7 +16177,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/moon.jpg", + "src": "moon.jpg", "alt": "moon", "title": null, "uploading": false, @@ -16187,7 +16187,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16231,7 +16231,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16251,7 +16251,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16305,7 +16305,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16328,7 +16328,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16373,7 +16373,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16414,7 +16414,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16567,7 +16567,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -16608,7 +16608,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -16667,7 +16667,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -16698,7 +16698,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -16743,7 +16743,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -16812,7 +16812,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url1", + "href": "/url1", "target": "_blank", "class": null, "title": null, @@ -16940,7 +16940,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -16995,7 +16995,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/uri", + "href": "/uri", "target": "_blank", "class": null, "title": null, @@ -17070,7 +17070,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17111,7 +17111,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17130,7 +17130,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17171,7 +17171,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17212,7 +17212,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17257,7 +17257,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17298,7 +17298,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17317,7 +17317,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17362,7 +17362,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17381,7 +17381,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17430,7 +17430,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -17471,7 +17471,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -17512,7 +17512,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -17604,7 +17604,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -17631,7 +17631,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url2", + "href": "/url2", "target": "_blank", "class": null, "title": null, @@ -17686,7 +17686,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url1", + "href": "/url1", "target": "_blank", "class": null, "title": null, @@ -17768,7 +17768,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url1", + "href": "/url1", "target": "_blank", "class": null, "title": null, @@ -17817,7 +17817,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": null, @@ -17858,7 +17858,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url2", + "href": "/url2", "target": "_blank", "class": null, "title": null, @@ -17874,7 +17874,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url1", + "href": "/url1", "target": "_blank", "class": null, "title": null, @@ -17933,7 +17933,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url1", + "href": "/url1", "target": "_blank", "class": null, "title": null, @@ -17985,7 +17985,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo", "title": "title", "uploading": false, @@ -18006,7 +18006,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/train.jpg", + "src": "train.jpg", "alt": "foo bar", "title": "train & tracks", "uploading": false, @@ -18041,7 +18041,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url2", + "src": "/url2", "alt": "foo bar", "title": null, "uploading": false, @@ -18062,7 +18062,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url2", + "src": "/url2", "alt": "foo bar", "title": null, "uploading": false, @@ -18083,7 +18083,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/train.jpg", + "src": "train.jpg", "alt": "foo bar", "title": "train & tracks", "uploading": false, @@ -18118,7 +18118,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/train.jpg", + "src": "train.jpg", "alt": "foo bar", "title": "train & tracks", "uploading": false, @@ -18153,7 +18153,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/train.jpg", + "src": "train.jpg", "alt": "foo", "title": null, "uploading": false, @@ -18178,7 +18178,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/path/to/train.jpg", + "src": "/path/to/train.jpg", "alt": "foo bar", "title": "title", "uploading": false, @@ -18199,7 +18199,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "url", "alt": "foo", "title": null, "uploading": false, @@ -18220,7 +18220,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "", "title": null, "uploading": false, @@ -18241,7 +18241,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo", "title": null, "uploading": false, @@ -18276,7 +18276,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo", "title": null, "uploading": false, @@ -18311,7 +18311,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo", "title": "title", "uploading": false, @@ -18346,7 +18346,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo bar", "title": "title", "uploading": false, @@ -18381,7 +18381,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "Foo", "title": "title", "uploading": false, @@ -18416,7 +18416,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo", "title": "title", "uploading": false, @@ -18455,7 +18455,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo", "title": "title", "uploading": false, @@ -18490,7 +18490,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "foo bar", "title": "title", "uploading": false, @@ -18549,7 +18549,7 @@ { "type": "image", "attrs": { - "src": "http://test.host/url", + "src": "/url", "alt": "Foo", "title": "title", "uploading": false, @@ -18620,7 +18620,7 @@ { "type": "link", "attrs": { - "href": "http://test.host/url", + "href": "/url", "target": "_blank", "class": null, "title": "title", @@ -18661,7 +18661,7 @@ { "type": "link", "attrs": { - "href": "http://foo.bar.baz/", + "href": "http://foo.bar.baz", "target": "_blank", "class": null, "title": null, @@ -18715,11 +18715,11 @@ { "type": "link", "attrs": { - "href": "irc://foo.bar:2233/baz", + "href": null, "target": "_blank", "class": null, "title": null, - "canonicalSrc": "irc://foo.bar:2233/baz" + "canonicalSrc": null } } ], @@ -18742,7 +18742,7 @@ { "type": "link", "attrs": { - "href": "mailto:FOO@BAR.BAZ", + "href": "MAILTO:FOO@BAR.BAZ", "target": "_blank", "class": null, "title": null, @@ -18769,11 +18769,11 @@ { "type": "link", "attrs": { - "href": "a+b+c:d", + "href": null, "target": "_blank", "class": null, "title": null, - "canonicalSrc": "a+b+c:d" + "canonicalSrc": null } } ], @@ -18796,11 +18796,11 @@ { "type": "link", "attrs": { - "href": "made-up-scheme://foo,bar", + "href": null, "target": "_blank", "class": null, "title": null, - "canonicalSrc": "made-up-scheme://foo,bar" + "canonicalSrc": null } } ], @@ -18850,11 +18850,11 @@ { "type": "link", "attrs": { - "href": "localhost:5001/foo", + "href": null, "target": "_blank", "class": null, "title": null, - "canonicalSrc": "localhost:5001/foo" + "canonicalSrc": null } } ], @@ -19047,7 +19047,7 @@ { "type": "link", "attrs": { - "href": "http://foo.bar/", + "href": "http://foo.bar", "target": "_blank", "class": null, "title": null, @@ -19108,7 +19108,7 @@ { "type": "link", "attrs": { - "href": "http://example.com/", + "href": "http://example.com", "target": "_blank", "class": null, "title": null, @@ -19162,7 +19162,7 @@ { "type": "link", "attrs": { - "href": "http://www.commonmark.org/", + "href": "http://www.commonmark.org", "target": "_blank", "class": null, "title": null, @@ -19228,7 +19228,7 @@ { "type": "link", "attrs": { - "href": "http://www.commonmark.org/", + "href": "http://www.commonmark.org", "target": "_blank", "class": null, "title": null, @@ -19504,7 +19504,7 @@ { "type": "link", "attrs": { - "href": "http://commonmark.org/", + "href": "http://commonmark.org", "target": "_blank", "class": null, "title": null, diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index 0b0100c7d7f..6c6cba43155 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -678,6 +678,15 @@ module API desc: 'Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }' } ], + 'pumble' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Pumble chat webhook. For example, https://api.pumble.com/workspaces/x/...' + }, + chat_notification_events + ].flatten, 'pushover' => [ { required: true, diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb index c07c2c1994e..563fb3358ed 100644 --- a/lib/api/issue_links.rb +++ b/lib/api/issue_links.rb @@ -37,7 +37,7 @@ module API requires :target_project_id, type: String, desc: 'The ID of the target project' requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue' optional :link_type, type: String, values: IssueLink.link_types.keys, - desc: 'The type of the relation' + desc: 'The type of the relation' end # rubocop: disable CodeReuse/ActiveRecord post ':id/issues/:issue_iid/links' do diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 971163c18db..b6ad34424a6 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -16,7 +16,7 @@ module API optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :milestone, type: String, desc: 'Milestone title' optional :milestone_id, types: String, values: %w[Any None Upcoming Started], - desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")' + desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")' mutually_exclusive :milestone_id, :milestone optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues' @@ -27,8 +27,8 @@ module API optional :assignee_id, type: Integer, desc: 'Return issues which are not assigned to the user with the given ID' optional :assignee_username, type: Array[String], check_assignees_count: true, - coerce_with: Validations::Validators::CheckAssigneesCount.coerce, - desc: 'Return issues which are not assigned to the user with the given username' + coerce_with: Validations::Validators::CheckAssigneesCount.coerce, + desc: 'Return issues which are not assigned to the user with the given username' mutually_exclusive :assignee_id, :assignee_username use :negatable_issue_filter_params_ee @@ -40,7 +40,7 @@ module API # 'milestone_id' only accepts wildcard values 'Any', 'None', 'Upcoming', 'Started' # the param has '_id' in the name to keep consistency (ex. assignee_id accepts id and wildcard values). optional :milestone_id, types: String, values: %w[Any None Upcoming Started], - desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")' + desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")' optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues' optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' @@ -51,10 +51,10 @@ module API mutually_exclusive :author_id, :author_username optional :assignee_id, types: [Integer, String], integer_none_any: true, - desc: 'Return issues which are assigned to the user with the given ID' + desc: 'Return issues which are assigned to the user with the given ID' optional :assignee_username, type: Array[String], check_assignees_count: true, - coerce_with: Validations::Validators::CheckAssigneesCount.coerce, - desc: 'Return issues which are assigned to the user with the given username' + coerce_with: Validations::Validators::CheckAssigneesCount.coerce, + desc: 'Return issues which are assigned to the user with the given username' mutually_exclusive :assignee_id, :assignee_username optional :created_after, type: DateTime, desc: 'Return issues created after the specified time' @@ -77,13 +77,13 @@ module API params :issues_params do optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' + desc: 'Return opened, closed, or all issues' optional :order_by, type: String, values: Helpers::IssuesHelpers.sort_options, default: 'created_at', - desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.' + desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return issues sorted in `asc` or `desc` order.' + desc: 'Return issues sorted in `asc` or `desc` order.' optional :due_date, type: String, values: %w[0 any today tomorrow overdue week month next_month_and_previous_two_weeks] << '', - desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`' + desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`' optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}" use :issues_stats_params diff --git a/lib/api/labels.rb b/lib/api/labels.rb index e2d4f5d823a..0a107a96d61 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -23,11 +23,11 @@ module API end params do optional :with_counts, type: Boolean, default: false, - desc: 'Include issue and merge request counts' + desc: 'Include issue and merge request counts' optional :include_ancestor_groups, type: Boolean, default: true, - desc: 'Include ancestor groups' + desc: 'Include ancestor groups' optional :search, type: String, - desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6' + desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6' use :pagination end get ':id/labels' do @@ -40,7 +40,7 @@ module API end params do optional :include_ancestor_groups, type: Boolean, default: true, - desc: 'Include ancestor groups' + desc: 'Include ancestor groups' end get ':id/labels/:name' do get_label(user_project, Entities::ProjectLabel, declared_params) diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index c2ed23102a5..fb0221ee907 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -283,12 +283,12 @@ module API '' else file_params = { - file: params[:file], - size: params['file.size'], + file: params[:file], + size: params['file.size'], file_name: file_name, file_type: params['file.type'], file_sha1: params['file.sha1'], - file_md5: params['file.md5'] + file_md5: params['file.md5'] } ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)).execute diff --git a/lib/api/members.rb b/lib/api/members.rb index 0e93ab5435a..d26fdd09ee7 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -156,9 +156,9 @@ module API params do requires :user_id, type: Integer, desc: 'The user ID of the member' optional :skip_subresources, type: Boolean, default: false, - desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped' + desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped' optional :unassign_issuables, type: Boolean, default: false, - desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project' + desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project' end # rubocop: disable CodeReuse/ActiveRecord delete ":id/members/:user_id", feature_category: feature_category do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 6bd13c48ab1..a8f58e91067 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -159,7 +159,7 @@ module API params do use :merge_requests_params optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects', - default: true + default: true end get ":id/merge_requests", feature_category: :code_review, urgency: :low do validate_anonymous_search_access! if declared_params[:search].present? diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb index 6fc90da87d4..478adcdce70 100644 --- a/lib/api/metrics/dashboard/annotations.rb +++ b/lib/api/metrics/dashboard/annotations.rb @@ -20,11 +20,11 @@ module API resource annotations_source[:resource] do params do requires :starting_at, type: DateTime, - desc: 'Date time indicating starting moment to which the annotation relates.' + desc: 'Date time indicating starting moment to which the annotation relates.' optional :ending_at, type: DateTime, - desc: 'Date time indicating ending moment to which the annotation relates.' + desc: 'Date time indicating ending moment to which the annotation relates.' requires :dashboard_path, type: String, coerce_with: -> (val) { CGI.unescape(val) }, - desc: 'The path to a file defining the dashboard on which the annotation should be added' + desc: 'The path to a file defining the dashboard on which the annotation should be added' requires :description, type: String, desc: 'The description of the annotation' end diff --git a/lib/api/metrics/user_starred_dashboards.rb b/lib/api/metrics/user_starred_dashboards.rb index 83d95f8b062..4d5396acccb 100644 --- a/lib/api/metrics/user_starred_dashboards.rb +++ b/lib/api/metrics/user_starred_dashboards.rb @@ -13,7 +13,7 @@ module API params do requires :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) }, - desc: 'Url encoded path to a file defining the dashboard to which the star should be added' + desc: 'Url encoded path to a file defining the dashboard to which the star should be added' end post ':id/metrics/user_starred_dashboards' do @@ -30,7 +30,7 @@ module API params do optional :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) }, - desc: 'Url encoded path to a file defining the dashboard from which the star should be removed' + desc: 'Url encoded path to a file defining the dashboard from which the star should be removed' end delete ':id/metrics/user_starred_dashboards' do diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index d75ed3a48d7..2fd3239b44a 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -14,12 +14,12 @@ module API params :list_params do optional :state, type: String, values: %w[active closed all], default: 'all', - desc: 'Return "active", "closed", or "all" milestones' + desc: 'Return "active", "closed", or "all" milestones' optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IIDs of the milestones' optional :title, type: String, desc: 'The title of the milestones' optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' optional :include_parent_milestones, type: Grape::API::Boolean, default: false, - desc: 'Include group milestones from parent and its ancestors' + desc: 'Include group milestones from parent and its ancestors' use :pagination end @@ -27,7 +27,7 @@ module API requires :milestone_id, type: Integer, desc: 'The milestone ID number' optional :title, type: String, desc: 'The title of the milestone' optional :state_event, type: String, values: %w[close activate], - desc: 'The state event of the milestone ' + desc: 'The state event of the milestone ' use :optional_params at_least_one_of :title, :description, :start_date, :due_date, :state_event end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 2a854bd785e..59f57179871 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -30,7 +30,7 @@ module API optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return notes sorted in `asc` or `desc` order.' optional :activity_filter, type: String, values: UserPreference::NOTES_FILTERS.stringify_keys.keys, default: 'all_notes', - desc: 'The type of notables which are returned.' + desc: 'The type of notables which are returned.' use :pagination end # rubocop: disable CodeReuse/ActiveRecord diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 2e7f8475509..34d3a5150da 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -97,7 +97,7 @@ module API optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate optional :key, types: [File, String], desc: 'The key', as: :user_provided_key optional :auto_ssl_enabled, allow_blank: false, type: Boolean, default: false, - desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains." + desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains." # rubocop:enable Scalability/FileUploads all_or_none_of :user_provided_certificate, :user_provided_key end @@ -123,7 +123,7 @@ module API optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate optional :key, types: [File, String], desc: 'The key', as: :user_provided_key optional :auto_ssl_enabled, allow_blank: true, type: Boolean, - desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains." + desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains." # rubocop:enable Scalability/FileUploads end put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 79a5ca531e1..21b2b17d234 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -32,9 +32,9 @@ module API optional :package_name, type: String, desc: 'Return packages with this name' optional :include_versionless, type: Boolean, - desc: 'Returns packages without a version' + desc: 'Returns packages without a version' optional :status, type: String, values: Packages::Package.statuses.keys, - desc: 'Return packages with specified status' + desc: 'Return packages with specified status' end get ':id/packages' do packages = ::Packages::PackagesFinder.new( diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index fe0e837c596..f6e1286d616 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -37,7 +37,7 @@ module API params do requires :name, type: String, desc: 'The name of the template' optional :source_template_project_id, type: Integer, - desc: 'The project id where a given template is being stored. This is useful when multiple templates from different projects have the same name' + desc: 'The project id where a given template is being stored. This is useful when multiple templates from different projects have the same name' optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses' optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses' end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6530887c1c3..6ed480518ee 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -688,11 +688,11 @@ module API optional :search, type: String, desc: 'Return list of groups matching the search criteria' optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list' optional :with_shared, type: Boolean, default: false, - desc: 'Include shared groups' + desc: 'Include shared groups' optional :shared_visible_only, type: Boolean, default: false, - desc: 'Limit to shared groups user has access to' + desc: 'Limit to shared groups user has access to' optional :shared_min_access_level, type: Integer, values: Gitlab::Access.all_values, - desc: 'Limit returned shared groups by minimum access level to the project' + desc: 'Limit returned shared groups by minimum access level to the project' use :pagination end get ':id/groups', feature_category: :source_code_management do diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index a4f5dfefae6..38bafac25b2 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -61,8 +61,8 @@ module API values: ProtectedBranch::MergeAccessLevel.allowed_access_levels, desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)' optional :allow_force_push, type: Boolean, - default: false, - desc: 'Allow force push for all users with push access.' + default: false, + desc: 'Allow force push for all users with push access.' use :optional_params_ee end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index aecd6f9eef8..d7e497bddf3 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -23,9 +23,9 @@ module API params do requires :id, type: Integer, desc: 'The ID of the group to get releases for' optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return projects sorted in ascending and descending order by released_at' + desc: 'Return projects sorted in ascending and descending order by released_at' optional :simple, type: Boolean, default: false, - desc: 'Return only the ID, URL, name, and path of each project' + desc: 'Return only the ID, URL, name, and path of each project' use :pagination end @@ -61,7 +61,7 @@ module API optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return releases sorted in `asc` or `desc` order.' optional :include_html_description, type: Boolean, - desc: 'If `true`, a response includes HTML rendered markdown of the release description.' + desc: 'If `true`, a response includes HTML rendered markdown of the release description.' end route_setting :authentication, job_token_allowed: true get ':id/releases' do @@ -89,7 +89,7 @@ module API params do requires :tag_name, type: String, desc: 'The name of the tag', as: :tag optional :include_html_description, type: Boolean, - desc: 'If `true`, a response includes HTML rendered markdown of the release description.' + desc: 'If `true`, a response includes HTML rendered markdown of the release description.' end route_setting :authentication, job_token_allowed: true get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb index e6c54faebd9..85bbd0879b7 100644 --- a/lib/api/rubygem_packages.rb +++ b/lib/api/rubygem_packages.rb @@ -109,7 +109,7 @@ module API ).execute(:rubygems, name: ::Packages::Rubygems::TEMPORARY_PACKAGE_NAME) file_params = { - file: params[:file], + file: params[:file], file_name: PACKAGE_FILENAME } diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index bca1376d489..e279e63181d 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -22,14 +22,14 @@ module API def process_metrics Sidekiq::ProcessSet.new(false).map do |process| { - hostname: process['hostname'], - pid: process['pid'], - tag: process['tag'], - started_at: Time.at(process['started_at']), - queues: process['queues'], - labels: process['labels'], + hostname: process['hostname'], + pid: process['pid'], + tag: process['tag'], + started_at: Time.at(process['started_at']), + queues: process['queues'], + labels: process['labels'], concurrency: process['concurrency'], - busy: process['busy'] + busy: process['busy'] } end end diff --git a/lib/api/users.rb b/lib/api/users.rb index d66d86a9055..c93c0f601a0 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -68,9 +68,9 @@ module API params :sort_params do optional :order_by, type: String, values: %w[id name username created_at updated_at], - default: 'id', desc: 'Return users ordered by a field' + default: 'id', desc: 'Return users ordered by a field' optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return users sorted in ascending and descending order' + desc: 'Return users sorted in ascending and descending order' end end @@ -940,7 +940,7 @@ module API params do requires :name, type: String, desc: 'The name of the personal access token' requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s), - desc: 'The array of scopes of the personal access token' + desc: 'The array of scopes of the personal access token' optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token' end post feature_category: :authentication_and_authorization do diff --git a/lib/backup/gitaly_backup.rb b/lib/backup/gitaly_backup.rb index b777b581ae1..57dd74c7950 100644 --- a/lib/backup/gitaly_backup.rb +++ b/lib/backup/gitaly_backup.rb @@ -94,7 +94,7 @@ module Backup def build_env { 'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file, - 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir + 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir }.merge(ENV) end diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb index 521fd7bf4cc..1ca38d2612d 100644 --- a/lib/banzai/filter/references/abstract_reference_filter.rb +++ b/lib/banzai/filter/references/abstract_reference_filter.rb @@ -240,11 +240,11 @@ module Banzai object_parent_type = parent.is_a?(Group) ? :group : :project { - original: escape_html_entities(text), - link: link_content, - link_reference: link_reference, + original: escape_html_entities(text), + link: link_content, + link_reference: link_reference, object_parent_type => parent.id, - object_sym => object.id + object_sym => object.id } end diff --git a/lib/banzai/reference_redactor.rb b/lib/banzai/reference_redactor.rb index c19f992078a..0c031ace977 100644 --- a/lib/banzai/reference_redactor.rb +++ b/lib/banzai/reference_redactor.rb @@ -41,8 +41,8 @@ module Banzai nodes_for_document = entry[:nodes] doc_data = { - document: entry[:document], - total_reference_count: nodes_for_document.count, + document: entry[:document], + total_reference_count: nodes_for_document.count, visible_reference_count: nodes_for_document.count } diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb index 3a6b9ccdc33..c075ada725a 100644 --- a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb +++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb @@ -38,13 +38,14 @@ module Gitlab def initialize(report_type, report_version) @report_type = report_type.to_sym @report_version = report_version.to_s + @supported_versions = SUPPORTED_VERSIONS[@report_type] end delegate :validate, to: :schemer private - attr_reader :report_type, :report_version + attr_reader :report_type, :report_version, :supported_versions def schemer JSONSchemer.schema(pathname) @@ -60,10 +61,24 @@ module Gitlab report_declared_version = File.join(root_path, report_version, file_name) return report_declared_version if File.file?(report_declared_version) + if latest_vendored_patch_version + latest_vendored_patch_version_file = File.join(root_path, latest_vendored_patch_version, file_name) + return latest_vendored_patch_version_file if File.file?(latest_vendored_patch_version) + end + earliest_supported_version = SUPPORTED_VERSIONS[report_type].min File.join(root_path, earliest_supported_version, file_name) end + def latest_vendored_patch_version + ::Security::ReportSchemaVersionMatcher.new( + report_declared_version: report_version, + supported_versions: supported_versions + ).call + rescue ArgumentError + nil + end + def file_name report_type == :api_fuzzing ? "dast-report-format.json" : "#{report_type.to_s.dasherize}-report-format.json" end @@ -79,16 +94,80 @@ module Gitlab @warnings = [] @deprecation_warnings = [] - populate_errors - populate_warnings + populate_schema_version_errors + populate_validation_errors populate_deprecation_warnings end - def valid? - errors.empty? + def populate_schema_version_errors + add_schema_version_errors if add_schema_version_error? end - def populate_errors + def add_schema_version_errors + if report_version.nil? + template = _("Report version not provided,"\ + " %{report_type} report type supports versions: %{supported_schema_versions}."\ + " GitLab will attempt to validate this report against the earliest supported versions of this report"\ + " type, to show all the errors but will not ingest the report") + message = format(template, report_type: report_type, supported_schema_versions: supported_schema_versions) + else + template = _("Version %{report_version} for report type %{report_type} is unsupported, supported versions"\ + " for this report type are: %{supported_schema_versions}."\ + " GitLab will attempt to validate this report against the earliest supported versions of this report"\ + " type, to show all the errors but will not ingest the report") + message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions) + end + + log_warnings(problem_type: 'using_unsupported_schema_version') + add_message_as(level: :error, message: message) + end + + def add_schema_version_error? + !report_uses_supported_schema_version? && + !report_uses_deprecated_schema_version? && + !report_uses_supported_major_and_minor_schema_version? + end + + def report_uses_deprecated_schema_version? + DEPRECATED_VERSIONS[report_type].include?(report_version) + end + + def report_uses_supported_schema_version? + SUPPORTED_VERSIONS[report_type].include?(report_version) + end + + def report_uses_supported_major_and_minor_schema_version? + if !find_latest_patch_version.nil? + add_supported_major_minor_behavior_warning + true + else + false + end + end + + def find_latest_patch_version + ::Security::ReportSchemaVersionMatcher.new( + report_declared_version: report_version, + supported_versions: SUPPORTED_VERSIONS[report_type] + ).call + rescue ArgumentError + nil + end + + def add_supported_major_minor_behavior_warning + template = _("This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\ + " any vendored schema version. Validation will be attempted against version"\ + " %{find_latest_patch_version}") + + message = format(template, find_latest_patch_version: find_latest_patch_version) + + add_message_as( + level: :warning, + message: message + ) + end + + def populate_validation_errors schema_validation_errors = schema.validate(report_data).map { |error| JSONSchemer::Errors.pretty(error) } log_warnings(problem_type: 'schema_validation_fails') unless schema_validation_errors.empty? @@ -96,10 +175,6 @@ module Gitlab @errors += schema_validation_errors end - def populate_warnings - add_unsupported_report_version_message if !report_uses_supported_schema_version? && !report_uses_deprecated_schema_version? - end - def populate_deprecation_warnings add_deprecated_report_version_message if report_uses_deprecated_schema_version? end @@ -107,10 +182,19 @@ module Gitlab def add_deprecated_report_version_message log_warnings(problem_type: 'using_deprecated_schema_version') - message = "Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this report type are: #{supported_schema_versions}" + template = _("Version %{report_version} for report type %{report_type} has been deprecated,"\ + " supported versions for this report type are: %{supported_schema_versions}."\ + " GitLab will attempt to parse and ingest this report if valid.") + + message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions) + add_message_as(level: :deprecation_warning, message: message) end + def valid? + errors.empty? + end + def log_warnings(problem_type:) Gitlab::AppLogger.info( message: 'security report schema validation problem', @@ -123,30 +207,6 @@ module Gitlab ) end - def add_unsupported_report_version_message - log_warnings(problem_type: 'using_unsupported_schema_version') - - handle_unsupported_report_version - end - - def report_uses_deprecated_schema_version? - DEPRECATED_VERSIONS[report_type].include?(report_version) - end - - def report_uses_supported_schema_version? - SUPPORTED_VERSIONS[report_type].include?(report_version) - end - - def handle_unsupported_report_version - if report_version.nil? - message = "Report version not provided, #{report_type} report type supports versions: #{supported_schema_versions}" - else - message = "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: #{supported_schema_versions}" - end - - add_message_as(level: :error, message: message) - end - def supported_schema_versions SUPPORTED_VERSIONS[report_type].join(", ") end diff --git a/lib/security/report_schema_version_matcher.rb b/lib/security/report_schema_version_matcher.rb new file mode 100644 index 00000000000..d8eb5b8f490 --- /dev/null +++ b/lib/security/report_schema_version_matcher.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +module Security + class ReportSchemaVersionMatcher + def initialize(report_declared_version:, supported_versions:) + @report_version = Gem::Version.new(report_declared_version) + @supported_versions = supported_versions.sort.map { |version| Gem::Version.new(version) } + end + + attr_reader :report_version, :supported_versions + + def call + find_matching_versions + end + + private + + def find_matching_versions + dependency = Gem::Dependency.new('', approximate_version) + matches = supported_versions.map do |supported_version| + exact_version = ['', supported_version.to_s] + [supported_version.to_s, dependency.match?(*exact_version)] + end + matches.to_h.select { |_, matches_dependency| matches_dependency == true }.keys.max + end + + def approximate_version + "~> #{generate_patch_version}" + end + + def generate_patch_version + # We can't use #approximate_recommendation here because + # for "14.0.32" it would yield "~> 14.0" and according to + # https://www.rubydoc.info/github/rubygems/rubygems/Gem/Version#label-Preventing+Version+Catastrophe-3A + # "~> 3.0" covers [3.0...4.0) + # and version 14.1.0 would fall within that range + # + # Instead we replace the patch number with 0 and get "~> 14.0.0" + # Which will work as we want it to + (report_version.segments[0...2] << 0).join('.') + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a111a33859e..f109bfd7db6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -31894,6 +31894,12 @@ msgstr "" msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue." msgstr "" +msgid "PumbleIntegration|Send notifications about project events to Pumble." +msgstr "" + +msgid "PumbleIntegration|Send notifications about project events to Pumble. %{docs_link}" +msgstr "" + msgid "Purchase more minutes" msgstr "" @@ -32837,6 +32843,9 @@ msgstr "" msgid "Report for the scan has been removed from the database." msgstr "" +msgid "Report version not provided, %{report_type} report type supports versions: %{supported_schema_versions}. GitLab will attempt to validate this report against the earliest supported versions of this report type, to show all the errors but will not ingest the report" +msgstr "" + msgid "Report your license usage data to GitLab" msgstr "" @@ -40324,6 +40333,9 @@ msgstr "" msgid "This release was created with a date in the past. Evidence collection at the moment of the release is unavailable." msgstr "" +msgid "This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match any vendored schema version. Validation will be attempted against version %{find_latest_patch_version}" +msgstr "" + msgid "This repository" msgstr "" @@ -42970,6 +42982,12 @@ msgstr "" msgid "Version" msgstr "" +msgid "Version %{report_version} for report type %{report_type} has been deprecated, supported versions for this report type are: %{supported_schema_versions}. GitLab will attempt to parse and ingest this report if valid." +msgstr "" + +msgid "Version %{report_version} for report type %{report_type} is unsupported, supported versions for this report type are: %{supported_schema_versions}. GitLab will attempt to validate this report against the earliest supported versions of this report type, to show all the errors but will not ingest the report" +msgstr "" + msgid "Version %{versionNumber}" msgstr "" diff --git a/package.json b/package.json index 484759d8dea..32ce90c40ee 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,8 @@ "@babel/preset-env": "^7.18.2", "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", - "@gitlab/svgs": "2.33.0", - "@gitlab/ui": "42.25.0", + "@gitlab/svgs": "3.1.0", + "@gitlab/ui": "43.5.0", "@gitlab/visual-review-tools": "1.7.3", "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index fe804dc52d7..1baa97096d9 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -182,12 +182,14 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do context 'email confirmation disabled' do let(:send_email_confirmation) { false } - it 'signs up and redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do - fill_in_sign_up_form(new_user) - fill_in_welcome_form + context 'the user signs up for an account with the invitation email address' do + it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do + fill_in_sign_up_form(new_user) + fill_in_welcome_form - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) - expect(page).to have_content('You have been granted Owner access to group Owned.') + expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect(page).to have_content('You have been granted Owner access to group Owned.') + end end context 'the user sign-up using a different email address' do @@ -227,11 +229,13 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do end end - it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do - fill_in_sign_up_form(new_user) - fill_in_welcome_form + context 'the user signs up for an account with the invitation email address' do + it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do + fill_in_sign_up_form(new_user) + fill_in_welcome_form - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + end end context 'the user sign-up using a different email address' do diff --git a/spec/features/oauth_registration_spec.rb b/spec/features/oauth_registration_spec.rb index 18dd10755b1..cb8343b8065 100644 --- a/spec/features/oauth_registration_spec.rb +++ b/spec/features/oauth_registration_spec.rb @@ -85,7 +85,46 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection do expect(page).to have_content('Please complete your profile with email address') end end + + context 'when registering via an invitation email' do + let_it_be(:owner) { create(:user) } + let_it_be(:group) { create(:group, name: 'Owned') } + let_it_be(:project) { create(:project, :repository, namespace: group) } + + let(:invite_email) { generate(:email) } + let(:extra_params) { { invite_type: Emails::Members::INITIAL_INVITE } } + let(:group_invite) do + create( + :group_member, :invited, + group: group, + invite_email: invite_email, + created_by: owner + ) + end + + before do + project.add_maintainer(owner) + group.add_owner(owner) + group_invite.generate_invite_token! + + mock_auth_hash(provider, uid, invite_email, additional_info: additional_info) + end + + it 'redirects to the activity page with all the projects/groups invitations accepted' do + visit invite_path(group_invite.raw_invite_token, extra_params) + click_link_or_button "oauth-login-#{provider}" + fill_in_welcome_form + + expect(page).to have_content('You have been granted Owner access to group Owned.') + expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + end + end end end end + + def fill_in_welcome_form + select 'Software Developer', from: 'user_role' + click_button 'Get started!' + end end diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js index a5007e18f5f..70ed9eeb3e1 100644 --- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js +++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js @@ -87,8 +87,8 @@ describe('Delete user modal', () => { }); it('has disabled buttons', () => { - expect(findPrimaryButton().attributes('disabled')).toBeTruthy(); - expect(findSecondaryButton().attributes('disabled')).toBeTruthy(); + expect(findPrimaryButton().attributes('disabled')).toBe('true'); + expect(findSecondaryButton().attributes('disabled')).toBe('true'); }); }); @@ -105,8 +105,8 @@ describe('Delete user modal', () => { }); it('has disabled buttons', () => { - expect(findPrimaryButton().attributes('disabled')).toBeTruthy(); - expect(findSecondaryButton().attributes('disabled')).toBeTruthy(); + expect(findPrimaryButton().attributes('disabled')).toBe('true'); + expect(findSecondaryButton().attributes('disabled')).toBe('true'); }); }); @@ -123,8 +123,8 @@ describe('Delete user modal', () => { }); it('has enabled buttons', () => { - expect(findPrimaryButton().attributes('disabled')).toBeFalsy(); - expect(findSecondaryButton().attributes('disabled')).toBeFalsy(); + expect(findPrimaryButton().attributes('disabled')).toBeUndefined(); + expect(findSecondaryButton().attributes('disabled')).toBeUndefined(); }); describe('when primary action is clicked', () => { diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js index 42c6501dcce..6681ab91a4a 100644 --- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js @@ -58,7 +58,7 @@ describe('Ci variable modal', () => { }); it('button is disabled when no key/value pair are present', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + expect(findAddorUpdateButton().attributes('disabled')).toBe('true'); }); }); @@ -71,7 +71,7 @@ describe('Ci variable modal', () => { }); it('button is enabled when key/value pair are present', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); }); it('Add variable button dispatches addVariable action', () => { @@ -249,7 +249,7 @@ describe('Ci variable modal', () => { }); it('disables the submit button', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled'); }); it('shows the correct error text', () => { @@ -316,7 +316,7 @@ describe('Ci variable modal', () => { }); it('does not disable the submit button', () => { - expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); }); }); }); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index ca552644258..0f9073dc26d 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -261,7 +261,7 @@ describe('Client side Markdown processing', () => { ...source('foo'), alt: 'foo', canonicalSrc: 'bar', - src: 'http://test.host/bar', + src: 'bar', }), ), ), @@ -283,7 +283,7 @@ describe('Client side Markdown processing', () => { image({ ...source('foo'), alt: 'foo', - src: 'http://test.host/bar', + src: 'bar', canonicalSrc: 'bar', }), ), @@ -297,7 +297,7 @@ describe('Client side Markdown processing', () => { link( { ...source('[GitLab](https://gitlab.com "Go to GitLab")'), - href: 'https://gitlab.com/', + href: 'https://gitlab.com', canonicalSrc: 'https://gitlab.com', title: 'Go to GitLab', }, @@ -316,7 +316,7 @@ describe('Client side Markdown processing', () => { link( { ...source('[GitLab](https://gitlab.com "Go to GitLab")'), - href: 'https://gitlab.com/', + href: 'https://gitlab.com', canonicalSrc: 'https://gitlab.com', title: 'Go to GitLab', }, @@ -335,7 +335,7 @@ describe('Client side Markdown processing', () => { { ...source('www.commonmark.org'), canonicalSrc: 'http://www.commonmark.org', - href: 'http://www.commonmark.org/', + href: 'http://www.commonmark.org', }, 'www.commonmark.org', ), @@ -389,7 +389,7 @@ describe('Client side Markdown processing', () => { sourceMapKey: null, sourceMarkdown: null, canonicalSrc: 'https://gitlab.com', - href: 'https://gitlab.com/', + href: 'https://gitlab.com', }, 'https://gitlab.com', ), @@ -616,7 +616,7 @@ two ...source('![bar](foo.png)'), alt: 'bar', canonicalSrc: 'foo.png', - src: 'http://test.host/foo.png', + src: 'foo.png', }), ), ), @@ -969,12 +969,12 @@ Paragraph { ...source('[![moon](moon.jpg)](/uri)'), canonicalSrc: '/uri', - href: 'http://test.host/uri', + href: '/uri', }, image({ ...source('![moon](moon.jpg)'), canonicalSrc: 'moon.jpg', - src: 'http://test.host/moon.jpg', + src: 'moon.jpg', alt: 'moon', }), ), @@ -1010,7 +1010,7 @@ Paragraph { ...source('[moon](moon.jpg)'), canonicalSrc: 'moon.jpg', - href: 'http://test.host/moon.jpg', + href: 'moon.jpg', }, 'moon', ), @@ -1021,7 +1021,7 @@ Paragraph link( { ...source('[sun](sun.jpg)'), - href: 'http://test.host/sun.jpg', + href: 'sun.jpg', canonicalSrc: 'sun.jpg', }, 'sun', @@ -1141,7 +1141,7 @@ _world_. link( { ...source('[GitLab][gitlab-url]'), - href: 'https://gitlab.com/', + href: 'https://gitlab.com', canonicalSrc: 'https://gitlab.com', title: 'GitLab', }, @@ -1235,4 +1235,72 @@ body { expect(tiptapEditor.getHTML()).toEqual(expectedHtml); }, ); + + describe('attribute sanitization', () => { + // eslint-disable-next-line no-script-url + const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');"; + // eslint-disable-next-line no-script-url + const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');"; + + const docWithImageFactory = (urlInput, urlOutput) => { + const input = ``; + + return { + input, + expectedDoc: doc( + paragraph( + source(input), + image({ + ...source(input), + src: urlOutput, + canonicalSrc: urlOutput, + }), + ), + ), + }; + }; + + const docWithLinkFactory = (urlInput, urlOutput) => { + const input = `foo`; + + return { + input, + expectedDoc: doc( + paragraph( + source(input), + link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'), + ), + ), + }; + }; + + it.each` + desc | urlInput | urlOutput + ${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null} + ${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null} + ${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null} + ${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null} + ${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null} + ${'protocol-based JS injection: hex encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long hex encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: hex encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null} + ${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null} + ${'protocol-based JS injection: spaces and entities'} | ${" javascript:alert('XSS');"} | ${null} + ${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null} + ${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"} + ${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"} + ${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"} + `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => { + const exampleFactories = [docWithImageFactory, docWithLinkFactory]; + + exampleFactories.forEach(async (exampleFactory) => { + const { input, expectedDoc } = exampleFactory(urlInput, urlOutput); + const document = await deserialize(input); + + expect(document.toJSON()).toEqual(expectedDoc.toJSON()); + }); + }); + }); }); diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 55f17727df7..37fe2482123 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -1213,47 +1213,47 @@ paragraph }; it.each` - mark | markdown | modifiedMarkdown | editAction - ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction} - ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction} - ${'bold'} | ${'bold'} | ${'bold modified'} | ${defaultEditAction} - ${'bold'} | ${'bold'} | ${'bold modified'} | ${defaultEditAction} - ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction} - ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction} - ${'italic'} | ${'italic'} | ${'italic modified'} | ${defaultEditAction} - ${'italic'} | ${'italic'} | ${'italic modified'} | ${defaultEditAction} - ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction} - ${'link'} | ${'link'} | ${'link modified'} | ${defaultEditAction} - ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction} - ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction} - ${'link'} | ${'link '} | ${'modified link '} | ${prependContentEditAction} - ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction} - ${'link'} | ${'link '} | ${'modified link [https://www.gitlab.com>](https://www.gitlab.com%3E)'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction} - ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction} - ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction} - ${'code'} | ${'code'} | ${'code modified'} | ${defaultEditAction} - ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction} - ${'strike'} | ${'striked'} | ${'striked modified'} | ${defaultEditAction} - ${'strike'} | ${'striked'} | ${'striked modified'} | ${defaultEditAction} - ${'strike'} | ${'striked'} | ${'striked modified'} | ${defaultEditAction} - ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction} - ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction} - ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction} - ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction} - ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction} - ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction} - ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction} - ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction} - ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction} - ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction} + mark | markdown | modifiedMarkdown | editAction + ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction} + ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction} + ${'bold'} | ${'bold'} | ${'bold modified'} | ${defaultEditAction} + ${'bold'} | ${'bold'} | ${'bold modified'} | ${defaultEditAction} + ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction} + ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction} + ${'italic'} | ${'italic'} | ${'italic modified'} | ${defaultEditAction} + ${'italic'} | ${'italic'} | ${'italic modified'} | ${defaultEditAction} + ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction} + ${'link'} | ${'link'} | ${'link modified'} | ${defaultEditAction} + ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction} + ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction} + ${'link'} | ${'link '} | ${'modified link '} | ${prependContentEditAction} + ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link '} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction} + ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction} + ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction} + ${'code'} | ${'code'} | ${'code modified'} | ${defaultEditAction} + ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction} + ${'strike'} | ${'striked'} | ${'striked modified'} | ${defaultEditAction} + ${'strike'} | ${'striked'} | ${'striked modified'} | ${defaultEditAction} + ${'strike'} | ${'striked'} | ${'striked modified'} | ${defaultEditAction} + ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction} + ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction} + ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction} + ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction} + ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction} + ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction} + ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction} + ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction} + ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction} + ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction} `( 'preserves original $mark syntax when sourceMarkdown is available for $markdown', async ({ markdown, modifiedMarkdown, editAction }) => { diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 27604b8ccf3..12f9707da04 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -119,7 +119,7 @@ describe('Issuable output', () => { expect(findEdited().exists()).toBe(true); expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/); - expect(findEdited().props('updatedAt')).toBeTruthy(); + expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at); expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version); }) .then(() => { @@ -133,7 +133,7 @@ describe('Issuable output', () => { expect(findEdited().exists()).toBe(true); expect(findEdited().props('updatedByName')).toBe('Other User'); expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/); - expect(findEdited().props('updatedAt')).toBeTruthy(); + expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at); }); }); diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js index 136a5967ee4..b0218a9df12 100644 --- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js +++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js @@ -148,7 +148,7 @@ describe('ProjectDropdown', () => { }); it('emits `error` event', () => { - expect(wrapper.emitted('error')).toBeTruthy(); + expect(wrapper.emitted('error')).toHaveLength(1); }); }); diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js index 70df05a2781..6cfbdb16111 100644 --- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js +++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js @@ -124,7 +124,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { }); it('clicked on link with view', () => { - expect(primaryLink.props('menuItem').view).toBeTruthy(); + expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace); }); it('changes active view', () => { diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index 3350609bb90..59e2f15faa4 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -357,7 +357,7 @@ describe('issue_note', () => { createWrapper(); updateActions(); wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params); - expect(wrapper.emitted('handleUpdateNote')).toBeTruthy(); + expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1); }); it('does not stringify empty position', () => { diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js index 6c743f92116..f958f12acd4 100644 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js @@ -102,7 +102,7 @@ describe('Pipelines filtered search', () => { it('emits filterPipelines on submit with correct filter', () => { findFilteredSearch().vm.$emit('submit', mockSearch); - expect(wrapper.emitted('filterPipelines')).toBeTruthy(); + expect(wrapper.emitted('filterPipelines')).toHaveLength(1); expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]); }); diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js index fc906194059..a079b0b97fd 100644 --- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js +++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js @@ -50,39 +50,33 @@ describe('PrometheusMetrics', () => { customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LOADING); expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toEqual(false); - expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy(); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); - expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); }); it('should show metrics list when called with `list`', () => { customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true); expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); }); it('should show empty state when called with `empty`', () => { customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toEqual(false); - expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy(); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false); expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toEqual(false); @@ -94,14 +88,12 @@ describe('PrometheusMetrics', () => { const $metricsListLi = customMetrics.$monitoredCustomMetricsList.find('li'); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false); - expect( - customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'), - ).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true); expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); expect($metricsListLi.length).toEqual(metrics.length); }); @@ -114,10 +106,10 @@ describe('PrometheusMetrics', () => { false, ); - expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy(); - expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy(); + expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true); + expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true); + expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true); + expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true); }); }); }); diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js index 0df2aad5882..a65cbe1a47a 100644 --- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js @@ -54,25 +54,25 @@ describe('PrometheusMetrics', () => { it('should show loading state when called with `loading`', () => { prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true); }); it('should show metrics list when called with `list`', () => { prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false); }); it('should show empty state when called with `empty`', () => { prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true); }); }); @@ -88,8 +88,8 @@ describe('PrometheusMetrics', () => { const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li'); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false); expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual( '3 exporters with 12 metrics were found', @@ -102,8 +102,8 @@ describe('PrometheusMetrics', () => { it('should show missing environment variables list', () => { prometheusMetrics.populateActiveMetrics(missingVarMetrics); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBe(false); expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2'); expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2); @@ -143,12 +143,12 @@ describe('PrometheusMetrics', () => { prometheusMetrics.loadActiveMetrics(); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false); expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); await waitForPromises(); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); }); it('should show empty state if response failed to load', async () => { @@ -158,8 +158,8 @@ describe('PrometheusMetrics', () => { await waitForPromises(); - expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); - expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false); }); it('should populate metrics list once response is loaded', async () => { diff --git a/spec/graphql/types/projects/service_type_enum_spec.rb b/spec/graphql/types/projects/service_type_enum_spec.rb index ead69e60f6c..f7256910bb0 100644 --- a/spec/graphql/types/projects/service_type_enum_spec.rb +++ b/spec/graphql/types/projects/service_type_enum_spec.rb @@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['ServiceType'] do PIPELINES_EMAIL_SERVICE PIVOTALTRACKER_SERVICE PROMETHEUS_SERVICE + PUMBLE_SERVICE PUSHOVER_SERVICE REDMINE_SERVICE SHIMO_SERVICE diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb index aaac75e072f..77611c98179 100644 --- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -68,6 +68,49 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do describe '#valid?' do subject { validator.valid? } + context 'when given a supported MAJOR.MINOR schema version' do + let(:report_type) { :dast } + let(:report_version) do + latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".") + (latest_vendored_version[0...2] << "34").join(".") + end + + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } + end + + it { is_expected.to be_truthy } + end + + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } + end + + it { is_expected.to be_falsey } + + it 'logs related information' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, + security_report_failure: 'schema_validation_fails', + security_report_scanner_id: 'gemnasium', + security_report_scanner_version: '2.1.0' + ) + + subject + end + end + end + context 'when given a supported schema version' do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } @@ -320,6 +363,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do context 'when given an unsupported schema version' do let(:report_type) { :dast } let(:report_version) { "12.37.0" } + let(:expected_unsupported_message) do + "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: "\ + "#{supported_dast_versions}. GitLab will attempt to validate this report against the earliest supported "\ + "versions of this report type, to show all the errors but will not ingest the report" + end context 'and the report is valid' do let(:report_data) do @@ -331,7 +379,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ - "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}" + expected_unsupported_message ] end @@ -347,7 +395,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ - "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}", + expected_unsupported_message, "root is missing required keys: vulnerabilities" ] end @@ -359,6 +407,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do context 'when not given a schema version' do let(:report_type) { :dast } let(:report_version) { nil } + let(:expected_missing_version_message) do + "Report version not provided, #{report_type} report type supports versions: #{supported_dast_versions}. GitLab "\ + "will attempt to validate this report against the earliest supported versions of this report type, to show all "\ + "the errors but will not ingest the report" + end + let(:report_data) do { 'vulnerabilities' => [] @@ -368,7 +422,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do let(:expected_errors) do [ "root is missing required keys: version", - "Report version not provided, dast report type supports versions: #{supported_dast_versions}" + expected_missing_version_message ] end @@ -414,9 +468,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do end let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } + let(:expected_deprecation_message) do + "Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this "\ + "report type are: #{supported_dast_versions}. GitLab will attempt to parse and ingest this report if valid." + end + let(:expected_deprecation_warnings) do [ - "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}" + expected_deprecation_message ] end @@ -464,6 +523,62 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do describe '#warnings' do subject { validator.warnings } + context 'when given a supported MAJOR.MINOR schema version' do + let(:report_type) { :dast } + let(:report_version) do + latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".") + (latest_vendored_version[0...2] << "34").join(".") + end + + let(:latest_patch_version) do + ::Security::ReportSchemaVersionMatcher.new( + report_declared_version: report_version, + supported_versions: described_class::SUPPORTED_VERSIONS[report_type] + ).call + end + + let(:message) do + "This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\ + " any vendored schema version. Validation will be attempted against version"\ + " #{latest_patch_version}" + end + + context 'and the report is valid' do + let(:report_data) do + { + 'version' => report_version, + 'vulnerabilities' => [] + } + end + + it { is_expected.to match_array([message]) } + end + + context 'and the report is invalid' do + let(:report_data) do + { + 'version' => report_version + } + end + + it { is_expected.to match_array([message]) } + + it 'logs related information' do + expect(Gitlab::AppLogger).to receive(:info).with( + message: "security report schema validation problem", + security_report_type: report_type, + security_report_version: report_version, + project_id: project.id, + security_report_failure: 'schema_validation_fails', + security_report_scanner_id: 'gemnasium', + security_report_scanner_version: '2.1.0' + ) + + subject + end + end + end + context 'when given a supported schema version' do let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 0d03f30a6d2..1a270fb8523 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -597,6 +597,7 @@ project: - alert_management_alerts - repository_storage_moves - freeze_periods +- pumble_integration - webex_teams_integration - build_report_results - vulnerability_statistic diff --git a/spec/lib/security/report_schema_version_matcher_spec.rb b/spec/lib/security/report_schema_version_matcher_spec.rb new file mode 100644 index 00000000000..9c40f0bc6fa --- /dev/null +++ b/spec/lib/security/report_schema_version_matcher_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Security::ReportSchemaVersionMatcher do + let(:vendored_versions) { %w[14.0.0 14.0.1 14.0.2 14.1.0] } + let(:version_finder) do + described_class.new( + report_declared_version: report_version, + supported_versions: vendored_versions + ) + end + + describe '#call' do + subject { version_finder.call } + + context 'when minor version matches' do + context 'and report schema patch version does not match any vendored schema versions' do + context 'and report version is 14.1.1' do + let(:report_version) { '14.1.1' } + + it 'returns 14.1.0' do + expect(subject).to eq('14.1.0') + end + end + + context 'and report version is 14.0.32' do + let(:report_version) { '14.0.32' } + + it 'returns 14.0.2' do + expect(subject).to eq('14.0.2') + end + end + end + end + + context 'when report minor version does not match' do + let(:report_version) { '14.2.1' } + + it 'does not return a version' do + expect(subject).to be_nil + end + end + end +end diff --git a/spec/models/integrations/pumble_spec.rb b/spec/models/integrations/pumble_spec.rb new file mode 100644 index 00000000000..8b9b5d214c6 --- /dev/null +++ b/spec/models/integrations/pumble_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Integrations::Pumble do + it_behaves_like "chat integration", "Pumble" do + let(:client_arguments) { webhook_url } + let(:payload) do + { + text: be_present + } + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 05651f7d85a..e2911f2201e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -44,6 +44,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_one(:mattermost_integration) } it { is_expected.to have_one(:hangouts_chat_integration) } it { is_expected.to have_one(:unify_circuit_integration) } + it { is_expected.to have_one(:pumble_integration) } it { is_expected.to have_one(:webex_teams_integration) } it { is_expected.to have_one(:packagist_integration) } it { is_expected.to have_one(:pushover_integration) } diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb index b2db7f7caef..1e8061f9606 100644 --- a/spec/requests/api/integrations_spec.rb +++ b/spec/requests/api/integrations_spec.rb @@ -66,6 +66,7 @@ RSpec.describe API::Integrations do mattermost: %i[deployment_channel labels_to_be_notified], mock_ci: %i[enable_ssl_verification], prometheus: %i[manual_configuration], + pumble: %i[branches_to_be_notified notify_only_broken_pipelines], slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified], unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines], webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines] diff --git a/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec b/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec index 05b9115b596..abbcaa268d0 100644 --- a/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec +++ b/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec @@ -8,9 +8,8 @@ Gem::Specification.new do |gem| gem.description = gem.summary gem.homepage = "https://github.com/tduehr/omniauth-cas3" - gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - gem.files = `git ls-files`.split("\n") - gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.files = Dir.glob("lib/**/*.*") + gem.test_files = Dir.glob("spec/**/**/*.*") gem.name = "omniauth-cas3" gem.require_paths = ["lib"] gem.version = Omniauth::Cas3::VERSION diff --git a/yarn.lock b/yarn.lock index 8820ef33e9d..a4083810bca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1051,15 +1051,15 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.2.0" -"@gitlab/svgs@2.33.0": - version "2.33.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.33.0.tgz#e970ae10ee558e1e2b01116b2fe6ea25161a4609" - integrity sha512-8B5pGmZ6QnywxmWCmqMTkJfPlETbx4R7AK7si8Jf2DyWZ7Agfg9NOdgBq++IuiVjbxBO7VTQcZbVSavxrce6QA== +"@gitlab/svgs@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.1.0.tgz#0108498a17e2f79d16158015db0be764b406cc09" + integrity sha512-kZ45VTQOgLdwQCLRSj7+aohF+6AUnAaoucR1CFY/6DPDLnNNGeflwsCLN0sFBKwx42HLxFfNwvDmKOMLdSQg5A== -"@gitlab/ui@42.25.0": - version "42.25.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.25.0.tgz#d79873347be9868c4d3d3123295ce1f12967f330" - integrity sha512-yxSQeLbhrPD4KKQPCo+glarlhoa4cj46j7mgQtTRbJFw2ZWPcpJ4xuujCb8GoyGPlHpWaS8VJyv3l+hwBQs3qg== +"@gitlab/ui@43.5.0": + version "43.5.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.5.0.tgz#c0652c99cd7ba9c69cef1cdf75c85b9164536e24" + integrity sha512-mbWXKylbnEuCXZuNMVic7K6Dvo8hjwYQkpyVvxdpmTCY+eTOtjxenVHE4HgZ5G/7cjznRnj4WLk0Ot8AquifBQ== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.20.1"