diff options
92 files changed, 1529 insertions, 322 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 454f5b6cbf2..235d02efa49 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -247,10 +247,14 @@ danger-review: script: - > if [ -z "$DANGER_GITLAB_API_TOKEN" ]; then - # Force danger to skip CI source GitLab and fallback to "local only git repo". - unset GITLAB_CI - # We need to base SHA to help danger determine the base commit for this shallow clone. - run_timed_command "bundle exec danger dry_run --fail-on-errors=true --verbose --base='$CI_MERGE_REQUEST_DIFF_BASE_SHA'" + run_timed_command danger_as_local else run_timed_command "bundle exec danger --fail-on-errors=true --verbose" fi + +danger-review-local: + extends: + - danger-review + - .review:rules:danger-local + script: + - run_timed_command danger_as_local diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 0817afe6cda..8d030b33cbc 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -330,6 +330,12 @@ - ".dockerignore" - "qa/**/*" +.code-backstage-danger-patterns: &code-backstage-danger-patterns + # Backstage changes + - "Dangerfile" + - "danger/**/*" + - "tooling/danger/**/*" + ################ # Shared rules # ################ @@ -1284,6 +1290,11 @@ rules: - if: '$CI_MERGE_REQUEST_IID' +.review:rules:danger-local: + rules: + - if: '$CI_MERGE_REQUEST_IID' + changes: *code-backstage-danger-patterns + ############### # Setup rules # ############### diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 63dba868a0c..7b3b6e02bb3 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -14.0.1 +14.1.0 diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index fde134f1440..11e6b4577e0 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -221,7 +221,7 @@ export default { } if (this.visibilityLevel !== visibilityOptions.PUBLIC) { - options.push([visibilityOptions.PUBLIC, PAGE_FEATURE_ACCESS_LEVEL]); + options.push([30, PAGE_FEATURE_ACCESS_LEVEL]); } } return options; diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 15d04dadb15..6d77952f24e 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -3,6 +3,35 @@ import $ from 'jquery'; /** * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable * and controllable by a public API. + * + * This component has two intervals: + * + * - current interval - when the page is visible - defined by `startingInterval`, `maxInterval`, and `incrementByFactorOf` + * - Example: + * - `startingInterval: 10000`, `maxInterval: 240000`, `incrementByFactorOf: 2` + * - results in `10s, 20s, 40s, 80s, ..., 240s`, it stops increasing at `240s` and keeps this interval indefinitely. + * - hidden interval - when the page is not visible + * + * Visibility transitions: + * + * - `visible -> not visible` + * - `document.addEventListener('visibilitychange', () => ...)` + * + * > This event fires with a visibilityState of hidden when a user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile, switches from the browser to a different app. + * + * Source [Document: visibilitychange event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event) + * + * - `window.addEventListener('blur', () => ...)` - every time user clicks somewhere else then in the browser page + * - `not visible -> visible` + * - `document.addEventListener('visibilitychange', () => ...)` same as the transition `visible -> not visible` + * - `window.addEventListener('focus', () => ...)` + * + * The combination of these two listeners can result in an unexpected resumption of polling: + * + * - switch to a different window (causes `blur`) + * - switch to a different desktop (causes `visibilitychange` (not visible)) + * - switch back to the original desktop (causes `visibilitychange` (visible)) + * - *now the polling happens even in window that user doesn't work in* */ export default class SmartInterval { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 963f1cf324f..5177eab790b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import createFlash from '~/flash'; import { s__, __ } from '~/locale'; import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants'; @@ -8,7 +8,6 @@ import modalEventHub from '~/projects/commit/event_hub'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import eventHub from '../../event_hub'; import MrWidgetAuthorTime from '../mr_widget_author_time.vue'; -import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetMerged', @@ -17,7 +16,7 @@ export default { }, components: { MrWidgetAuthorTime, - statusIcon, + GlIcon, ClipboardButton, GlLoadingIcon, GlButton, @@ -116,7 +115,7 @@ export default { </script> <template> <div class="mr-widget-body media"> - <status-icon status="success" /> + <gl-icon name="merge" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" /> <div class="media-body"> <div class="space-children"> <mr-widget-author-time @@ -131,7 +130,6 @@ export default { :title="revertTitle" size="small" category="secondary" - variant="warning" data-qa-selector="revert_button" @click="openRevertModal" > @@ -144,7 +142,6 @@ export default { :title="revertTitle" size="small" category="secondary" - variant="warning" data-method="post" > {{ revertLabel }} @@ -169,6 +166,15 @@ export default { > {{ cherryPickLabel }} </gl-button> + <gl-button + v-if="shouldShowRemoveSourceBranch" + :disabled="isMakingRequest" + size="small" + class="js-remove-branch-button" + @click="removeSourceBranch" + > + {{ s__('mrWidget|Delete source branch') }} + </gl-button> </div> <section class="mr-info-list" data-qa-selector="merged_status_content"> <p> @@ -196,17 +202,6 @@ export default { <p v-if="mr.sourceBranchRemoved"> {{ s__('mrWidget|The source branch has been deleted') }} </p> - <p v-if="shouldShowRemoveSourceBranch" class="space-children"> - <span>{{ s__('mrWidget|You can delete the source branch now') }}</span> - <gl-button - :disabled="isMakingRequest" - size="small" - class="js-remove-branch-button" - @click="removeSourceBranch" - > - {{ s__('mrWidget|Delete source branch') }} - </gl-button> - </p> <p v-if="shouldShowSourceBranchRemoving"> <gl-loading-icon size="sm" :inline="true" /> <span> {{ s__('mrWidget|The source branch is being deleted') }} </span> diff --git a/app/controllers/members/mailgun/permanent_failures_controller.rb b/app/controllers/members/mailgun/permanent_failures_controller.rb new file mode 100644 index 00000000000..685faa34694 --- /dev/null +++ b/app/controllers/members/mailgun/permanent_failures_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Members + module Mailgun + class PermanentFailuresController < ApplicationController + respond_to :json + + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + + before_action :ensure_feature_enabled! + before_action :authenticate_signature! + before_action :validate_invite_email! + + feature_category :authentication_and_authorization + + def create + webhook_processor.execute + + head :ok + end + + private + + def ensure_feature_enabled! + render_406 unless Gitlab::CurrentSettings.mailgun_events_enabled? + end + + def authenticate_signature! + access_denied! unless valid_signature? + end + + def valid_signature? + return false if Gitlab::CurrentSettings.mailgun_signing_key.blank? + + # per this guide: https://documentation.mailgun.com/en/latest/user_manual.html#webhooks + digest = OpenSSL::Digest.new('SHA256') + data = [params.dig(:signature, :timestamp), params.dig(:signature, :token)].join + + hmac_digest = OpenSSL::HMAC.hexdigest(digest, Gitlab::CurrentSettings.mailgun_signing_key, data) + + ActiveSupport::SecurityUtils.secure_compare(params.dig(:signature, :signature), hmac_digest) + end + + def validate_invite_email! + # permanent_failures webhook does not provide a way to filter failures, so we'll get them all on this endpoint + # and we only care about our invite_emails + render_406 unless payload[:tags]&.include?(::Members::Mailgun::INVITE_EMAIL_TAG) + end + + def webhook_processor + ::Members::Mailgun::ProcessWebhookService.new(payload) + end + + def payload + @payload ||= params.permit!['event-data'] + end + + def render_406 + # failure to stop retries per https://documentation.mailgun.com/en/latest/user_manual.html#webhooks + head :not_acceptable + end + end + end +end diff --git a/app/finders/container_repositories_finder.rb b/app/finders/container_repositories_finder.rb index 14e4d6799d8..1f6fa9aa1cc 100644 --- a/app/finders/container_repositories_finder.rb +++ b/app/finders/container_repositories_finder.rb @@ -25,8 +25,6 @@ class ContainerRepositoriesFinder end def project_repositories - return unless @subject.container_registry_enabled - @subject.container_repositories end diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb index 02995267dc2..50984415aa5 100644 --- a/app/helpers/packages_helper.rb +++ b/app/helpers/packages_helper.rb @@ -57,7 +57,7 @@ module PackagesHelper def show_cleanup_policy_on_alert(project) Gitlab.com? && Gitlab.config.registry.enabled && - project.container_registry_enabled && + project.feature_available?(:container_registry, current_user) && !Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries && Feature.enabled?(:container_expiration_policies_historic_entry, project) && project.container_expiration_policy.nil? && diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 9e3a6a60d75..77af6e37099 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -26,6 +26,12 @@ module SidebarsHelper Sidebars::Projects::Context.new(**context_data) end + def group_sidebar_context(group, user) + context_data = group_sidebar_context_data(group, user) + + Sidebars::Groups::Context.new(**context_data) + end + private def sidebar_attributes_for_object(object) @@ -89,6 +95,13 @@ module SidebarsHelper show_cluster_hint: show_gke_cluster_integration_callout?(project) } end + + def group_sidebar_context_data(group, user) + { + current_user: user, + container: group + } + end end SidebarsHelper.prepend_mod_with('SidebarsHelper') diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index d1870065845..738794a94e7 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -150,10 +150,10 @@ module Emails end def invite_email_headers - if Gitlab.dev_env_or_com? + if Gitlab::CurrentSettings.mailgun_events_enabled? { - 'X-Mailgun-Tag' => 'invite_email', - 'X-Mailgun-Variables' => { 'invite_token' => @token }.to_json + 'X-Mailgun-Tag' => ::Members::Mailgun::INVITE_EMAIL_TAG, + 'X-Mailgun-Variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => @token }.to_json } else {} diff --git a/app/models/ci/build_dependencies.rb b/app/models/ci/build_dependencies.rb index d39e0411a79..c4a04d42a1e 100644 --- a/app/models/ci/build_dependencies.rb +++ b/app/models/ci/build_dependencies.rb @@ -37,12 +37,20 @@ module Ci next [] unless processable.pipeline_id # we don't have any dependency when creating the pipeline deps = model_class.where(pipeline_id: processable.pipeline_id).latest - deps = from_previous_stages(deps) - deps = from_needs(deps) + deps = find_dependencies(processable, deps) + from_dependencies(deps).to_a end end + def find_dependencies(processable, deps) + if processable.scheduling_type_dag? + from_needs(deps) + else + from_previous_stages(deps) + end + end + # Dependencies from the same parent-pipeline hierarchy excluding # the current job's pipeline def cross_pipeline @@ -125,8 +133,6 @@ module Ci end def from_needs(scope) - return scope unless processable.scheduling_type_dag? - needs_names = processable.needs.artifacts.select(:name) scope.where(name: needs_names) end diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb index 6b5ba462b94..012dcc4418f 100644 --- a/app/models/error_tracking/error.rb +++ b/app/models/error_tracking/error.rb @@ -9,4 +9,15 @@ class ErrorTracking::Error < ApplicationRecord validates :name, presence: true validates :description, presence: true validates :actor, presence: true + + def self.report_error(name:, description:, actor:, platform:, timestamp:) + safe_find_or_create_by( + name: name, + description: description, + actor: actor, + platform: platform + ) do |error| + error.update!(last_seen_at: timestamp) + end + end end diff --git a/app/models/project.rb b/app/models/project.rb index 21d5b083476..6873c5f8236 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -377,6 +377,8 @@ class Project < ApplicationRecord has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient' has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList' + has_many :error_tracking_errors, inverse_of: :project, class_name: 'ErrorTracking::Error' + has_many :timelogs accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb index 2b611c857c7..b422e57baad 100644 --- a/app/services/ci/after_requeue_job_service.rb +++ b/app/services/ci/after_requeue_job_service.rb @@ -10,8 +10,16 @@ module Ci private def process_subsequent_jobs(processable) - processable.pipeline.processables.skipped.after_stage(processable.stage_idx).find_each do |processable| - process(processable) + if Feature.enabled?(:ci_same_stage_job_needs, processable.project, default_enabled: :yaml) + (stage_dependent_jobs(processable) | needs_dependent_jobs(processable)) + .each do |processable| + process(processable) + end + else + skipped_jobs(processable).after_stage(processable.stage_idx) + .find_each do |job| + process(job) + end end end @@ -24,5 +32,17 @@ module Ci processable.process(current_user) end end + + def skipped_jobs(processable) + processable.pipeline.processables.skipped + end + + def stage_dependent_jobs(processable) + skipped_jobs(processable).scheduling_type_stage.after_stage(processable.stage_idx) + end + + def needs_dependent_jobs(processable) + skipped_jobs(processable).scheduling_type_dag.with_needs([processable.name]) + end end end diff --git a/app/services/error_tracking/collect_error_service.rb b/app/services/error_tracking/collect_error_service.rb new file mode 100644 index 00000000000..bc1f238d81f --- /dev/null +++ b/app/services/error_tracking/collect_error_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ErrorTracking + class CollectErrorService < ::BaseService + def execute + # Error is a way to group events based on common data like name or cause + # of exception. We need to keep a sane balance here between taking too little + # and too much data into group logic. + error = project.error_tracking_errors.report_error( + name: exception['type'], # Example: ActionView::MissingTemplate + description: exception['value'], # Example: Missing template posts/show in... + actor: event['transaction'], # Example: PostsController#show + platform: event['platform'], # Example: ruby + timestamp: event['timestamp'] + ) + + # The payload field contains all the data on error including stacktrace in jsonb. + # Together with occured_at these are 2 main attributes that we need to save here. + error.events.create!( + environment: event['environment'], + description: exception['type'], + level: event['level'], + occurred_at: event['timestamp'], + payload: event + ) + end + + private + + def event + params[:event] + end + + def exception + event['exception']['values'].first + end + end +end diff --git a/app/services/members/mailgun.rb b/app/services/members/mailgun.rb new file mode 100644 index 00000000000..43fb5a14ef1 --- /dev/null +++ b/app/services/members/mailgun.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Members + module Mailgun + INVITE_EMAIL_TAG = 'invite_email' + INVITE_EMAIL_TOKEN_KEY = :invite_token + end +end diff --git a/app/services/members/mailgun/process_webhook_service.rb b/app/services/members/mailgun/process_webhook_service.rb new file mode 100644 index 00000000000..e359a83ad42 --- /dev/null +++ b/app/services/members/mailgun/process_webhook_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Members + module Mailgun + class ProcessWebhookService + ProcessWebhookServiceError = Class.new(StandardError) + + def initialize(payload) + @payload = payload + end + + def execute + @member = Member.find_by_invite_token(invite_token) + update_member_and_log if member + rescue ProcessWebhookServiceError => e + Gitlab::ErrorTracking.track_exception(e) + end + + private + + attr_reader :payload, :member + + def update_member_and_log + log_update_event if member.update(invite_email_success: false) + end + + def log_update_event + Gitlab::AppLogger.info "UPDATED MEMBER INVITE_EMAIL_SUCCESS: member_id: #{member.id}" + end + + def invite_token + # may want to validate schema in some way using ::JSONSchemer.schema(SCHEMA_PATH).valid?(message) if this + # gets more complex + payload.dig('user-variables', ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY) || + raise(ProcessWebhookServiceError, "Failed to receive #{::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY} in user-variables: #{payload}") + end + end + end +end diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml index 6204f7df5dc..40b4d5cac6d 100644 --- a/app/views/admin/application_settings/_mailgun.html.haml +++ b/app/views/admin/application_settings/_mailgun.html.haml @@ -1,5 +1,3 @@ -- return unless Feature.enabled?(:mailgun_events_receiver) - - expanded = integration_expanded?('mailgun_') %section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/clusters/clusters/_multiple_clusters_message.html.haml b/app/views/clusters/clusters/_multiple_clusters_message.html.haml index da3e128ba32..f235435d907 100644 --- a/app/views/clusters/clusters/_multiple_clusters_message.html.haml +++ b/app/views/clusters/clusters/_multiple_clusters_message.html.haml @@ -1,4 +1,4 @@ -- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'using-multiple-kubernetes-clusters') +- autodevops_help_url = help_page_path('topics/autodevops/index.md', anchor: 'use-multiple-kubernetes-clusters') - help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe - help_link_end = '</a>'.html_safe diff --git a/app/views/groups/_delete_project_button.html.haml b/app/views/groups/_delete_project_button.html.haml new file mode 100644 index 00000000000..54a99319418 --- /dev/null +++ b/app/views/groups/_delete_project_button.html.haml @@ -0,0 +1 @@ += link_to _('Delete'), project, data: { confirm: remove_project_message(project) }, method: :delete, class: "btn gl-button btn-danger" diff --git a/app/views/groups/_project_badges.html.haml b/app/views/groups/_project_badges.html.haml new file mode 100644 index 00000000000..1f7895e216c --- /dev/null +++ b/app/views/groups/_project_badges.html.haml @@ -0,0 +1,2 @@ +- if project.archived + %span.badge.badge-warning.badge-pill.gl-badge.md= _('archived') diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 9d595d19779..9dbf60b119c 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -15,13 +15,12 @@ .controls = link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button" = link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button" - = link_to _('Delete'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn gl-button btn-danger" + = render 'delete_project_button', project: project .stats %span.badge.badge-pill = storage_counter(project.statistics&.storage_size) - - if project.archived - %span.badge.badge-warning archived + = render 'project_badges', project: project .title = link_to(project_path(project)) do diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 014f3cf7241..980730bc3be 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,176 +1,3 @@ -- issues_count = cached_issuables_count(@group, type: :issues) -- merge_requests_count = cached_issuables_count(@group, type: :merge_requests) -- aside_title = @group.subgroup? ? _('Subgroup navigation') : _('Group navigation') - -%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@group), 'aria-label': aside_title } - .nav-sidebar-inner-scroll - %ul.sidebar-top-level-items.qa-group-sidebar - = nav_link(path: ['groups#show', 'groups#details'], html_options: { class: 'context-header' }) do - = link_to group_path(@group), title: @group.name, data: { qa_selector: 'group_scope_link' } do - %span{ class: ['avatar-container', 'rect-avatar', 'group-avatar' , 's32'] } - = group_icon(@group, class: ['avatar', 'avatar-tile', 's32']) - %span.sidebar-context-title - = @group.name - = render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group - - - if group_sidebar_link?(:overview) - - paths = group_overview_nav_link_paths - = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do - = link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do - .nav-icon-container - = sprite_icon('group') - %span.nav-item-name - = group_information_title(@group) - - %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} } - = nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do - = link_to activity_group_path(@group) do - %strong.fly-out-top-item-name - = group_information_title(@group) - %li.divider.fly-out-top-item - - - if group_sidebar_link?(:activity) - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: _('Activity') do - %span - = _('Activity') - - - if group_sidebar_link?(:labels) - = nav_link(path: 'labels#index') do - = link_to group_labels_path(@group), title: _('Labels') do - %span - = _('Labels') - - - if group_sidebar_link?(:group_members) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do - %span - = _('Members') - - = render_if_exists "layouts/nav/ee/epic_link", group: @group - - - if group_sidebar_link?(:issues) - = nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do - = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do - .nav-icon-container - = sprite_icon('issues') - %span.nav-item-name - = _('Issues') - %span.badge.badge-pill.count= issues_count - - %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} } - = nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do - = link_to issues_group_path(@group) do - %strong.fly-out-top-item-name - = _('Issues') - %span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count - - %li.divider.fly-out-top-item - = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do - = link_to issues_group_path(@group), title: _('List') do - %span - = _('List') - - - if group_sidebar_link?(:boards) - = nav_link(path: ['boards#index', 'boards#show']) do - = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do - %span - = boards_link_text - - - if group_sidebar_link?(:milestones) - = nav_link(path: 'milestones#index') do - = link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do - %span - = _('Milestones') - - = render_if_exists 'layouts/nav/sidebar/group_iterations_link' - - - if group_sidebar_link?(:merge_requests) - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group) do - .nav-icon-container - = sprite_icon('git-merge') - %span.nav-item-name - = _('Merge requests') - %span.badge.badge-pill.count= merge_requests_count - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do - = link_to merge_requests_group_path(@group) do - %strong.fly-out-top-item-name - = _('Merge requests') - %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count - - = render_if_exists "layouts/nav/ee/security_link" # EE-specific - - = render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific - - - if group_sidebar_link?(:kubernetes) - = nav_link(controller: [:clusters]) do - = link_to group_clusters_path(@group) do - .nav-icon-container - = sprite_icon('cloud-gear') - %span.nav-item-name - = _('Kubernetes') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do - = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do - %strong.fly-out-top-item-name - = _('Kubernetes') - - = render 'groups/sidebar/packages' - - = render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user) - - - if group_sidebar_link?(:wiki) - = render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url - - - if group_sidebar_link?(:settings) - = nav_link(path: group_settings_nav_link_paths) do - = link_to edit_group_path(@group), class: 'has-sub-items' do - .nav-icon-container - = sprite_icon('settings') - %span.nav-item-name{ data: { qa_selector: 'group_settings' } } - = _('Settings') - %ul.sidebar-sub-level-items.qa-group-sidebar-submenu{ data: { testid: 'group-settings-menu' } } - = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do - = link_to edit_group_path(@group) do - %strong.fly-out-top-item-name - = _('Settings') - %li.divider.fly-out-top-item - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do - %span - = _('General') - - = nav_link(controller: :integrations) do - = link_to group_settings_integrations_path(@group), title: _('Integrations') do - %span - = _('Integrations') - - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: _('Projects') do - %span - = _('Projects') - - = nav_link(controller: :repository) do - = link_to group_settings_repository_path(@group), title: _('Repository') do - %span - = _('Repository') - - = nav_link(controller: [:ci_cd, 'groups/runners']) do - = link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do - %span - = _('CI/CD') - - = nav_link(controller: :applications) do - = link_to group_settings_applications_path(@group), title: _('Applications') do - %span - = _('Applications') - - = render 'groups/sidebar/packages_settings' - - = render_if_exists "groups/ee/settings_nav" - - = render_if_exists "groups/ee/administration_nav" - - = render 'shared/sidebar_toggle_button' +-# We're migration the group sidebar to a logical model based structure. If you need to update +-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_group_menus.html.haml. += render partial: 'shared/nav/sidebar', object: Sidebars::Groups::Panel.new(group_sidebar_context(@group, current_user)) diff --git a/app/views/layouts/nav/sidebar/_group_menus.html.haml b/app/views/layouts/nav/sidebar/_group_menus.html.haml new file mode 100644 index 00000000000..5738c8becd5 --- /dev/null +++ b/app/views/layouts/nav/sidebar/_group_menus.html.haml @@ -0,0 +1,166 @@ +- issues_count = cached_issuables_count(@group, type: :issues) +- merge_requests_count = cached_issuables_count(@group, type: :merge_requests) + += render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group + +- if group_sidebar_link?(:overview) + - paths = group_overview_nav_link_paths + = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do + = link_to activity_group_path(@group), class: 'has-sub-items', data: { qa_selector: 'group_information_link' } do + .nav-icon-container + = sprite_icon('group') + %span.nav-item-name + = group_information_title(@group) + + %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_information_submenu'} } + = nav_link(path: paths, html_options: { class: "fly-out-top-item" } ) do + = link_to activity_group_path(@group) do + %strong.fly-out-top-item-name + = group_information_title(@group) + %li.divider.fly-out-top-item + + - if group_sidebar_link?(:activity) + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: _('Activity') do + %span + = _('Activity') + + - if group_sidebar_link?(:labels) + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: _('Labels') do + %span + = _('Labels') + + - if group_sidebar_link?(:group_members) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: _('Members'), data: { qa_selector: 'group_members_item' } do + %span + = _('Members') + += render_if_exists "layouts/nav/ee/epic_link", group: @group + +- if group_sidebar_link?(:issues) + = nav_link(path: group_issues_sub_menu_items, unless: -> { current_path?('issues_analytics#show') }) do + = link_to issues_group_path(@group), data: { qa_selector: 'group_issues_item' }, class: 'has-sub-items' do + .nav-icon-container + = sprite_icon('issues') + %span.nav-item-name + = _('Issues') + %span.badge.badge-pill.count= issues_count + + %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} } + = nav_link(path: group_issues_sub_menu_items, html_options: { class: "fly-out-top-item" } ) do + = link_to issues_group_path(@group) do + %strong.fly-out-top-item-name + = _('Issues') + %span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count + + %li.divider.fly-out-top-item + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: _('List') do + %span + = _('List') + + - if group_sidebar_link?(:boards) + = nav_link(path: ['boards#index', 'boards#show']) do + = link_to group_boards_path(@group), title: boards_link_text, data: { qa_selector: 'group_issue_boards_link' } do + %span + = boards_link_text + + - if group_sidebar_link?(:milestones) + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: _('Milestones'), data: { qa_selector: 'group_milestones_link' } do + %span + = _('Milestones') + + = render_if_exists 'layouts/nav/sidebar/group_iterations_link' + +- if group_sidebar_link?(:merge_requests) + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group) do + .nav-icon-container + = sprite_icon('git-merge') + %span.nav-item-name + = _('Merge requests') + %span.badge.badge-pill.count= merge_requests_count + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do + = link_to merge_requests_group_path(@group) do + %strong.fly-out-top-item-name + = _('Merge requests') + %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count + += render_if_exists "layouts/nav/ee/security_link" # EE-specific + += render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific + +- if group_sidebar_link?(:kubernetes) + = nav_link(controller: [:clusters]) do + = link_to group_clusters_path(@group) do + .nav-icon-container + = sprite_icon('cloud-gear') + %span.nav-item-name + = _('Kubernetes') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: [:clusters], html_options: { class: "fly-out-top-item" } ) do + = link_to group_clusters_path(@group), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do + %strong.fly-out-top-item-name + = _('Kubernetes') + += render 'groups/sidebar/packages' + += render 'layouts/nav/sidebar/analytics_links', links: group_analytics_navbar_links(@group, current_user) + +- if group_sidebar_link?(:wiki) + = render 'layouts/nav/sidebar/wiki_link', wiki_url: @group.wiki.web_url + +- if group_sidebar_link?(:settings) + = nav_link(path: group_settings_nav_link_paths) do + = link_to edit_group_path(@group), class: 'has-sub-items' do + .nav-icon-container + = sprite_icon('settings') + %span.nav-item-name{ data: { qa_selector: 'group_settings' } } + = _('Settings') + %ul.sidebar-sub-level-items{ data: { testid: 'group-settings-menu', qa_selector: 'group_sidebar_submenu' } } + = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show groups/applications#index], html_options: { class: "fly-out-top-item" } ) do + = link_to edit_group_path(@group) do + %strong.fly-out-top-item-name + = _('Settings') + %li.divider.fly-out-top-item + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: _('General'), data: { qa_selector: 'general_settings_link' } do + %span + = _('General') + + = nav_link(controller: :integrations) do + = link_to group_settings_integrations_path(@group), title: _('Integrations') do + %span + = _('Integrations') + + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: _('Projects') do + %span + = _('Projects') + + = nav_link(controller: :repository) do + = link_to group_settings_repository_path(@group), title: _('Repository') do + %span + = _('Repository') + + = nav_link(controller: [:ci_cd, 'groups/runners']) do + = link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do + %span + = _('CI/CD') + + = nav_link(controller: :applications) do + = link_to group_settings_applications_path(@group), title: _('Applications') do + %span + = _('Applications') + + = render 'groups/sidebar/packages_settings' + + = render_if_exists "groups/ee/settings_nav" + += render_if_exists "groups/ee/administration_nav" + += render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/sidebar/_group_scope_menu.html.haml b/app/views/layouts/nav/sidebar/_group_scope_menu.html.haml new file mode 100644 index 00000000000..57c0663f3ae --- /dev/null +++ b/app/views/layouts/nav/sidebar/_group_scope_menu.html.haml @@ -0,0 +1,6 @@ += nav_link(path: ['groups#show', 'groups#details'], html_options: { class: 'context-header' }) do + = link_to group_path(@group), title: @group.name, data: { qa_selector: 'group_scope_link' } do + %span{ class: ['avatar-container', 'rect-avatar', 'group-avatar' , 's32'] } + = group_icon(@group, class: ['avatar', 'avatar-tile', 's32']) + %span.sidebar-context-title + = @group.name diff --git a/app/views/shared/nav/_sidebar.html.haml b/app/views/shared/nav/_sidebar.html.haml index a52c2f8dd4b..915352996d9 100644 --- a/app/views/shared/nav/_sidebar.html.haml +++ b/app/views/shared/nav/_sidebar.html.haml @@ -1,13 +1,14 @@ %aside.nav-sidebar{ class: ('sidebar-collapsed-desktop' if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(sidebar.container), 'aria-label': sidebar.aria_label } .nav-sidebar-inner-scroll - - if sidebar.render_raw_scope_menu_partial - = render sidebar.render_raw_scope_menu_partial - %ul.sidebar-top-level-items{ data: { qa_selector: sidebar_qa_selector(sidebar.container) } } - - if sidebar.scope_menu + - if sidebar.render_raw_scope_menu_partial + = render sidebar.render_raw_scope_menu_partial + - elsif sidebar.scope_menu = render partial: 'shared/nav/scope_menu', object: sidebar.scope_menu + - if sidebar.renderable_menus.any? = render partial: 'shared/nav/sidebar_menu', collection: sidebar.renderable_menus + - if sidebar.render_raw_menus_partial = render sidebar.render_raw_menus_partial diff --git a/config/feature_flags/development/ci_same_stage_job_needs.yml b/config/feature_flags/development/ci_same_stage_job_needs.yml new file mode 100644 index 00000000000..a7247320d0d --- /dev/null +++ b/config/feature_flags/development/ci_same_stage_job_needs.yml @@ -0,0 +1,8 @@ +--- +name: ci_same_stage_job_needs +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59668 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328253 +milestone: '14.1' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/feature_flags/development/integrated_error_tracking.yml b/config/feature_flags/development/integrated_error_tracking.yml new file mode 100644 index 00000000000..7fc29492233 --- /dev/null +++ b/config/feature_flags/development/integrated_error_tracking.yml @@ -0,0 +1,8 @@ +--- +name: integrated_error_tracking +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65767 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335846 +milestone: '14.1' +type: development +group: group::monitor +default_enabled: false diff --git a/config/feature_flags/development/load_balancing_for_update_all_mirrors_worker.yml b/config/feature_flags/development/load_balancing_for_update_all_mirrors_worker.yml new file mode 100644 index 00000000000..1f213f52753 --- /dev/null +++ b/config/feature_flags/development/load_balancing_for_update_all_mirrors_worker.yml @@ -0,0 +1,8 @@ +--- +name: load_balancing_for_update_all_mirrors_worker +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64526 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334162 +milestone: '14.1' +type: development +group: group::source code +default_enabled: false diff --git a/config/feature_flags/development/mailgun_events_receiver.yml b/config/feature_flags/development/mailgun_events_receiver.yml deleted file mode 100644 index 119d8d34f21..00000000000 --- a/config/feature_flags/development/mailgun_events_receiver.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: mailgun_events_receiver -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64249 -rollout_issue_url: -milestone: '14.1' -type: development -group: group::expansion -default_enabled: false diff --git a/config/routes.rb b/config/routes.rb index c1cb5a2a26f..45df5593bd8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -222,6 +222,7 @@ Rails.application.routes.draw do draw :snippets draw :profile + draw :members # Product analytics collector match '/collector/i', to: ProductAnalytics::CollectorApp.new, via: :all diff --git a/config/routes/members.rb b/config/routes/members.rb new file mode 100644 index 00000000000..e84f0987171 --- /dev/null +++ b/config/routes/members.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +namespace :members do + namespace :mailgun do + resources :permanent_failures, only: [:create] + end +end diff --git a/db/migrate/20210630144339_add_invite_email_success_to_members.rb b/db/migrate/20210630144339_add_invite_email_success_to_members.rb new file mode 100644 index 00000000000..2476a9468fc --- /dev/null +++ b/db/migrate/20210630144339_add_invite_email_success_to_members.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddInviteEmailSuccessToMembers < ActiveRecord::Migration[6.1] + def change + add_column :members, :invite_email_success, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20210713135152_add_devops_adoption_vulnerability_management_used_count.rb b/db/migrate/20210713135152_add_devops_adoption_vulnerability_management_used_count.rb new file mode 100644 index 00000000000..74f24364177 --- /dev/null +++ b/db/migrate/20210713135152_add_devops_adoption_vulnerability_management_used_count.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDevopsAdoptionVulnerabilityManagementUsedCount < ActiveRecord::Migration[6.1] + def change + add_column :analytics_devops_adoption_snapshots, :vulnerability_management_used_count, :integer + end +end diff --git a/db/migrate/20210713144637_add_vulnerabilities_created_at_index.rb b/db/migrate/20210713144637_add_vulnerabilities_created_at_index.rb new file mode 100644 index 00000000000..27eb2691754 --- /dev/null +++ b/db/migrate/20210713144637_add_vulnerabilities_created_at_index.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddVulnerabilitiesCreatedAtIndex < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX_NAME = 'idx_vulnerabilities_partial_devops_adoption' + + def up + add_concurrent_index :vulnerabilities, [:project_id, :created_at], where: 'state != 1', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME + end +end diff --git a/db/schema_migrations/20210630144339 b/db/schema_migrations/20210630144339 new file mode 100644 index 00000000000..5d91d60199c --- /dev/null +++ b/db/schema_migrations/20210630144339 @@ -0,0 +1 @@ +8d1777941e1a4b5f9f8f5f5e3ae416d6d02aaee1174eff1f9b4b38a6cdf0103a
\ No newline at end of file diff --git a/db/schema_migrations/20210713135152 b/db/schema_migrations/20210713135152 new file mode 100644 index 00000000000..a1ba4e939e9 --- /dev/null +++ b/db/schema_migrations/20210713135152 @@ -0,0 +1 @@ +d7f8f7f5d8a6cf03d500825ef43234c69f7ad36908c0bade337591b05985c2fe
\ No newline at end of file diff --git a/db/schema_migrations/20210713144637 b/db/schema_migrations/20210713144637 new file mode 100644 index 00000000000..ebc122e0275 --- /dev/null +++ b/db/schema_migrations/20210713144637 @@ -0,0 +1 @@ +699ac7f8b9253920271686c497b57521bf4b0d26c802ca2a57447e4929cd147f
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e3d007d9b3d..07cbad0f69d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9139,6 +9139,7 @@ CREATE TABLE analytics_devops_adoption_snapshots ( dast_enabled_count integer, dependency_scanning_enabled_count integer, coverage_fuzzing_enabled_count integer, + vulnerability_management_used_count integer, CONSTRAINT check_3f472de131 CHECK ((namespace_id IS NOT NULL)) ); @@ -14661,7 +14662,8 @@ CREATE TABLE members ( requested_at timestamp without time zone, expires_at date, ldap boolean DEFAULT false NOT NULL, - override boolean DEFAULT false NOT NULL + override boolean DEFAULT false NOT NULL, + invite_email_success boolean DEFAULT true NOT NULL ); CREATE SEQUENCE members_id_seq @@ -22717,6 +22719,8 @@ CREATE UNIQUE INDEX idx_vuln_signatures_on_occurrences_id_and_signature_sha ON v CREATE UNIQUE INDEX idx_vuln_signatures_uniqueness_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, algorithm_type, signature_sha); +CREATE INDEX idx_vulnerabilities_partial_devops_adoption ON vulnerabilities USING btree (project_id, created_at) WHERE (state <> 1); + CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue ON vulnerability_external_issue_links USING btree (vulnerability_id, external_type, external_project_key, external_issue_key); CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_link_type ON vulnerability_external_issue_links USING btree (vulnerability_id, link_type) WHERE (link_type = 1); diff --git a/doc/administration/integration/mailgun.md b/doc/administration/integration/mailgun.md new file mode 100644 index 00000000000..6486cc9de04 --- /dev/null +++ b/doc/administration/integration/mailgun.md @@ -0,0 +1,41 @@ +--- +stage: Growth +group: Expansion +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 +type: reference, howto +--- + +# Mailgun and GitLab **(FREE SELF)** + +When you use Mailgun to send emails for your GitLab instance and [Mailgun](https://www.mailgun.com/) +integration is enabled and configured in GitLab, you can receive their webhook for +permanent invite email failures. To set up the integration, you must: + +1. [Configure your Mailgun domain](#configure-your-mailgun-domain). +1. [Enable Mailgun integration](#enable-mailgun-integration). + +After completing the integration, Mailgun `permanent_failure` webhooks are sent to your GitLab instance. + +## Configure your Mailgun domain + +Before you can enable Mailgun in GitLab, set up your own Mailgun permanent failure endpoint to receive the webhooks. + +Using the [Mailgun webhook guide](https://www.mailgun.com/blog/a-guide-to-using-mailguns-webhooks/): + +1. Add a webhook with the **Event type** set to **Permanent Failure**. +1. Fill in the URL of your instance and include the `/-/members/mailgun/permanent_failures` path. + - Example: `https://myinstance.gitlab.com/-/members/mailgun/permanent_failures` + +## Enable Mailgun integration + +After configuring your Mailgun domain for the permanent failures endpoint, +you're ready to enable the Mailgun integration: + +1. Sign in to GitLab as an [Administrator](../../user/permissions.md) user. +1. On the top bar, select **Menu >** **{admin}** **Admin**. +1. In the left sidebar, go to **Settings > General** and expand the **Mailgun** section. +1. Select the **Enable Mailgun** check box. +1. Enter the Mailgun HTTP webhook signing key as described in + [the Mailgun documentation](https://documentation.mailgun.com/en/latest/user_manual.html#webhooks) and + shown in the [API security](https://app.mailgun.com/app/account/security/api_keys) section for your Mailgun account. +1. Select **Save changes**. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 39100b25ccf..4a0572569a3 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -8555,6 +8555,7 @@ Snapshot. | <a id="devopsadoptionsnapshotsecurityscansucceeded"></a>`securityScanSucceeded` | [`Boolean!`](#boolean) | At least one security scan succeeded. | | <a id="devopsadoptionsnapshotstarttime"></a>`startTime` | [`Time!`](#time) | The start time for the snapshot where the data points were collected. | | <a id="devopsadoptionsnapshottotalprojectscount"></a>`totalProjectsCount` | [`Int`](#int) | Total number of projects. | +| <a id="devopsadoptionsnapshotvulnerabilitymanagementusedcount"></a>`vulnerabilityManagementUsedCount` | [`Int`](#int) | Total number of projects with vulnerability management used at least once. | ### `DiffPosition` diff --git a/doc/api/settings.md b/doc/api/settings.md index d49dca96dfd..14a018e687e 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -328,7 +328,7 @@ listed in the descriptions of the relevant settings. | `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Disabled by default.| | `keep_latest_artifact` | boolean | no | Prevent the deletion of the artifacts from the most recent successful jobs, regardless of the expiry time. Enabled by default. | | `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. | -| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook | +| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook. | | `mailgun_events_enabled` | boolean | no | Enable Mailgun event receiver. | | `maintenance_mode_message` | string | no | **(PREMIUM)** Message displayed when instance is in maintenance mode. | | `maintenance_mode` | boolean | no | **(PREMIUM)** When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. | diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index d1728ba2949..c9d111238b9 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -1563,6 +1563,14 @@ production: #### Requirements and limitations +- In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632) + you can refer to jobs in the same stage as the job you are configuring. This feature + is [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default. +- Disabled on GitLab.com. +- Not recommended for production use. +- For GitLab self-managed instances, GitLab adminsitrators + can choose to [disable it](#enable-or-disable-needs-for-jobs-in-the-same-stage) +- In GitLab 14.0 and older, you can only refer to jobs in earlier stages. - In GitLab 13.9 and older, if `needs:` refers to a job that might not be added to a pipeline because of `only`, `except`, or `rules`, the pipeline might fail to create. - The maximum number of jobs that a single job can need in the `needs:` array is limited: @@ -1579,6 +1587,22 @@ production: - Stages must be explicitly defined for all jobs that have the keyword `needs:` or are referred to by one. +##### Enable or disable `needs` for jobs in the same stage **(FREE SELF)** + +`needs` for jobs in the same stage is under development but ready for production use. +It is deployed behind a feature flag that is **enabled by default**. +[GitLab administrators with access to the GitLab Rails +console](../../administration/feature_flags.md) +can opt to disable it. + +To enable it: + +`Feature.enable(:ci_same_stage_job_needs)` + +To disable it: + +`Feature.disable(:ci_same_stage_job_needs)` + ##### Changing the `needs:` job limit **(FREE SELF)** The maximum number of jobs that can be defined in `needs:` defaults to 50. diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 59554023667..23ca57cb8b8 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -26,6 +26,11 @@ and the advantage of the [special searches](../user/search/advanced_search.md). | GitLab Enterprise Edition 9.0 through 11.4 | Elasticsearch 5.1 through 5.5 | | GitLab Enterprise Edition 8.4 through 8.17 | Elasticsearch 2.4 with [Delete By Query Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/2.4/plugins-delete-by-query.html) installed | +The Elasticsearch Integration is designed to work with supported versions of +Elasticsearch and follows Elasticsearch's [End of Life Policy](https://www.elastic.co/support/eol). +When we change Elasticsearch supported versions in GitLab, we announce them in [deprecation notes](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations) in monthly release posts +before the actual removal. + ## System requirements Elasticsearch requires additional resources in excess of those documented in the diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index ea1c716bd07..beb5f6a58f6 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -9,9 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - Introduced in GitLab 11.0 for general availability. GitLab Auto DevOps helps to reduce the complexity of software delivery by -setting up pipelines and integrations for you. Instead of requiring you to -manually configure your entire GitLab environment, Auto DevOps configures -many of these areas for you, including security auditing and vulnerability +setting up pipelines and integrations for you. Auto DevOps configures +GitLab CI/CD pipelines including security auditing and vulnerability testing. Using Auto DevOps, you can: @@ -54,17 +53,17 @@ following levels: | GitLab SaaS | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | | GitLab self-managed | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | -When you enable AutoDevOps for your instance, it attempts to run on all -pipelines in each project, but will automatically disable itself for individual +When you enable Auto DevOps for your instance, it attempts to run on all +pipelines in each project. The Auto DevOps setting automatically disables itself for individual projects on their first pipeline failure. An instance administrator can enable or disable this default in the [Auto DevOps settings](../../user/admin_area/settings/continuous_integration.md#auto-devops). -Since [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/issues/26655), +[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26655) in GitLab 12.7, Auto DevOps runs on pipelines automatically only if a [`Dockerfile` or matching buildpack](stages.md#auto-build) exists. If a [CI/CD configuration file](../../ci/yaml/index.md) is present in the -project, it isn't changed and won't be affected by Auto DevOps. +project, it remains unchanged and Auto DevOps doesn't affect it. ### At the project level @@ -88,9 +87,8 @@ After enabling the feature, an Auto DevOps pipeline is triggered on the default Only administrators and group owners can enable or disable Auto DevOps at the group level. -When enabling or disabling Auto DevOps at group level, group configuration is -implicitly used for the subgroups and projects inside that group, unless Auto DevOps -is specifically enabled or disabled on the subgroup or project. +When you enable Auto DevOps at group level, the subgroups and projects in that group inherit the configuration. Auto DevOps +can be specifically enabled or disabled individually for projects and subgroups. To enable or disable Auto DevOps at the group level: @@ -138,12 +136,12 @@ to minimize downtime and risk. ## Quick start -If you're using GitLab.com, see the [quick start guide](quick_start_guide.md) -for setting up Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes +For GitLab.com users, see the [quick start guide](quick_start_guide.md) +for setting up Auto DevOps deploying to a Kubernetes cluster on Google Kubernetes Engine (GKE). If you use a self-managed instance of GitLab, you must configure the -[Google OAuth2 OmniAuth Provider](../../integration/google.md) before +[Google OAuth 2.0 OmniAuth Provider](../../integration/google.md) before configuring a cluster on GKE. After configuring the provider, you can follow the steps in the [quick start guide](quick_start_guide.md) to get started. @@ -174,7 +172,7 @@ NOTE: Depending on your target platform, some features might not be available to you. Comprised of a set of [stages](stages.md), Auto DevOps brings these best practices to your -project in a simple and automatic way: +project automatically: - [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing) - [Auto Build](stages.md#auto-build) @@ -233,8 +231,7 @@ any of the following places: The base domain variable `KUBE_INGRESS_BASE_DOMAIN` follows the same order of precedence as other environment [variables](../../ci/variables/index.md#cicd-variable-precedence). -If the CI/CD variable is not set and the cluster setting is left blank, the instance-wide **Auto DevOps domain** -setting is used if set. +If this variable isn't set and the cluster setting is left blank, the instance-wide domain is used if set for your instance. Auto DevOps requires a wildcard DNS A record matching the base domain(s). For a base domain of `example.com`, you'd need a DNS entry like: @@ -259,14 +256,14 @@ to the Kubernetes pods running your application. See [Auto DevOps requirements for Amazon ECS](requirements.md#auto-devops-requirements-for-amazon-ecs). -## Using multiple Kubernetes clusters +## Use multiple Kubernetes clusters When using Auto DevOps, you can deploy different environments to different Kubernetes clusters, due to the 1:1 connection [existing between them](../../user/project/clusters/multiple_kubernetes_clusters.md). The [Deploy Job template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml) -used by Auto DevOps currently defines 3 environment names: +used by Auto DevOps defines 3 environment names: - `review/` (every environment starting with `review/`) - `staging` @@ -297,8 +294,8 @@ To add a different cluster for each environment: 1. Navigate to each cluster's page, through **Infrastructure > Kubernetes clusters**, and add the domain based on its Ingress IP address. -After completing configuration, you can test your setup by creating a merge request -and verifying your application is deployed as a Review App in the Kubernetes +After completing configuration, test your setup by creating a merge request. +Verify whether your application deployed as a Review App in the Kubernetes cluster with the `review/*` environment scope. Similarly, you can check the other environments. @@ -338,5 +335,23 @@ spec: value: "PUT_YOUR_HTTPS_PROXY_HERE" ``` +## Upgrade Auto DevOps dependencies when updating GitLab + +When updating GitLab, you may need to upgrade Auto DevOps dependencies to +match your new GitLab version: + +- [Upgrading Auto DevOps resources](upgrading_auto_deploy_dependencies.md): + - Auto DevOps template. + - Auto Deploy template. + - Auto Deploy image. + - Helm. + - Kubernetes. + - Environment variables. +- [Upgrading PostgreSQL](upgrading_postgresql.md). + +## Troubleshooting + +See [troubleshooting Auto DevOps](troubleshooting.md). + <!-- DO NOT ADD TROUBLESHOOTING INFO HERE --> <!-- Troubleshooting information has moved to troubleshooting.md --> diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 3a63c210e86..196f6dec7e7 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -314,7 +314,7 @@ all in GitLab. Despite its automatic nature, Auto DevOps can also be configured and customized to fit your workflow. Here are some helpful resources for further reading: 1. [Auto DevOps](index.md) -1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters) +1. [Multiple Kubernetes clusters](index.md#use-multiple-kubernetes-clusters) 1. [Incremental rollout to production](customize.md#incremental-rollout-to-production) **(PREMIUM)** 1. [Disable jobs you don't need with CI/CD variables](customize.md#cicd-variables) 1. [Use your own buildpacks to build your application](customize.md#custom-buildpacks) diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index d21b6c36224..17866dd30c4 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -39,6 +39,7 @@ To access the default page for Admin Area settings: | ------ | ----------- | | [Elasticsearch](../../../integration/elasticsearch.md#enabling-advanced-search) | Elasticsearch integration. Elasticsearch AWS IAM. | | [Kroki](../../../administration/integration/kroki.md#enable-kroki-in-gitlab) | Allow rendering of diagrams in AsciiDoc and Markdown documents using [kroki.io](https://kroki.io). | +| [Mailgun](../../../administration/integration/mailgun.md) | Enable your GitLab instance to receive invite email bounce events from Mailgun, if it is your email provider. | | [PlantUML](../../../administration/integration/plantuml.md) | Allow rendering of PlantUML diagrams in documents. | | [Slack application](../../../user/project/integrations/gitlab_slack_application.md#configuration) **(FREE SAAS)** | Slack integration allows you to interact with GitLab via slash commands in a chat window. This option is only available on GitLab.com, though it may be [available for self-managed instances in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/28164). | | [Third party offers](third_party_offers.md) | Control the display of third party offers. | diff --git a/lib/api/api.rb b/lib/api/api.rb index 659af98f861..f9e89191a36 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -167,6 +167,7 @@ module API mount ::API::Deployments mount ::API::Environments mount ::API::ErrorTracking + mount ::API::ErrorTrackingCollector mount ::API::Events mount ::API::FeatureFlags mount ::API::FeatureFlagsUserLists diff --git a/lib/api/error_tracking_collector.rb b/lib/api/error_tracking_collector.rb new file mode 100644 index 00000000000..08ff8d2e4d1 --- /dev/null +++ b/lib/api/error_tracking_collector.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module API + # This API is responsible for collecting error tracking information + # from sentry client. It allows us to use GitLab as an alternative to + # sentry backend. For more details see https://gitlab.com/gitlab-org/gitlab/-/issues/329596. + class ErrorTrackingCollector < ::API::Base + feature_category :error_tracking + + content_type :envelope, 'application/x-sentry-envelope' + default_format :envelope + + before do + not_found!('Project') unless project + not_found! unless feature_enabled? + end + + helpers do + def project + @project ||= find_project(params[:id]) + end + + def feature_enabled? + ::Feature.enabled?(:integrated_error_tracking, project) && + project.error_tracking_setting&.enabled? + end + end + + desc 'Submit error tracking event to the project' do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + requires :id, type: String, desc: 'The ID of a project' + end + post 'error_tracking/collector/api/:id/envelope' do + # There is a reason why we have such uncommon path. + # We depend on a client side error tracking software which + # modifies URL for its own reasons. + # + # When we give user a URL like this + # HOST/api/v4/error_tracking/collector/123 + # + # Then error tracking software will convert it like this: + # HOST/api/v4/error_tracking/collector/api/123/envelope/ + + begin + parsed_request = ::ErrorTracking::Collector::SentryRequestParser.parse(request) + rescue StandardError + render_api_error!('Failed to parse sentry request', 400) + end + + type = parsed_request[:request_type] + + # Sentry sends 2 requests on each exception: transaction and event. + # Everything else is not a desired behavior. + unless type == 'transaction' || type == 'event' + render_api_error!('400 Bad Request', 400) + + break + end + + # We don't have use for transaction request yet, + # so we record only event one. + if type == 'event' + ::ErrorTracking::CollectErrorService + .new(project, nil, event: parsed_request[:event]) + .execute + end + + no_content! + end + end +end diff --git a/lib/error_tracking/collector/sentry_request_parser.rb b/lib/error_tracking/collector/sentry_request_parser.rb new file mode 100644 index 00000000000..29e4cc8976f --- /dev/null +++ b/lib/error_tracking/collector/sentry_request_parser.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ErrorTracking + module Collector + class SentryRequestParser + def self.parse(request) + # Request body can be "" or "gzip". + # If later then body was compressed with Zlib.gzip + encoding = request.headers['Content-Encoding'] + + body = if encoding == 'gzip' + Zlib.gunzip(request.body.read) + else + request.body.read + end + + # Request body contains 3 json objects merged together in one StringIO. + # We need to separate and parse them into array of hash objects. + json_objects = [] + parser = Yajl::Parser.new + + parser.parse(body) do |json_object| + json_objects << json_object + end + + # The request contains 3 objects: sentry metadata, type data and event data. + # We need only last two. Type to decide what to do with the request. + # And event data as it contains all information about the exception. + _, type, event = json_objects + + { + request_type: type['type'], + event: event + } + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 3b2981135f8..fd05e542430 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -11,11 +11,16 @@ module Gitlab delegate :dig, to: :@seed_attributes - def initialize(context, attributes, previous_stages) + def initialize(context, attributes, previous_stages, current_stage) @context = context @pipeline = context.pipeline @seed_attributes = attributes - @previous_stages = previous_stages + @stages_for_needs_lookup = if Feature.enabled?(:ci_same_stage_job_needs, @pipeline.project, default_enabled: :yaml) + (previous_stages + [current_stage]).compact + else + previous_stages + end + @needs_attributes = dig(:needs_attributes) @resource_group_key = attributes.delete(:resource_group_key) @job_variables = @seed_attributes.delete(:job_variables) @@ -148,14 +153,18 @@ module Gitlab @needs_attributes.flat_map do |need| next if need[:optional] - result = @previous_stages.any? do |stage| - stage.seeds_names.include?(need[:name]) - end + result = need_present?(need) - "'#{name}' job needs '#{need[:name]}' job, but it was not added to the pipeline" unless result + "'#{name}' job needs '#{need[:name]}' job, but '#{need[:name]}' is not in any previous stage" unless result end.compact end + def need_present?(need) + @stages_for_needs_lookup.any? do |stage| + stage.seeds_names.include?(need[:name]) + end + end + def max_needs_allowed @pipeline.project.actual_limits.ci_needs_size_limit end diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index c988ea10e41..018fb260986 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -17,7 +17,7 @@ module Gitlab @previous_stages = previous_stages @builds = attributes.fetch(:builds).map do |attributes| - Seed::Build.new(context, attributes, previous_stages) + Seed::Build.new(context, attributes, previous_stages, self) end end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index a8c1002f2b9..c94fa84f608 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -46,6 +46,10 @@ module Gitlab @jobs.each do |name, job| validate_job!(name, job) end + + if ::Feature.enabled?(:ci_same_stage_job_needs, @opts[:project], default_enabled: :yaml) + YamlProcessor::Dag.check_circular_dependencies!(@jobs) + end end def validate_job!(name, job) @@ -99,10 +103,16 @@ module Gitlab job_stage_index = stage_index(name) dependency_stage_index = stage_index(dependency) - # A dependency might be defined later in the configuration - # with a stage that does not exist - unless dependency_stage_index.present? && dependency_stage_index < job_stage_index - error!("#{name} job: #{dependency_type} #{dependency} is not defined in prior stages") + if ::Feature.enabled?(:ci_same_stage_job_needs, @opts[:project], default_enabled: :yaml) + unless dependency_stage_index.present? && dependency_stage_index <= job_stage_index + error!("#{name} job: #{dependency_type} #{dependency} is not defined in current or prior stages") + end + else + # A dependency might be defined later in the configuration + # with a stage that does not exist + unless dependency_stage_index.present? && dependency_stage_index < job_stage_index + error!("#{name} job: #{dependency_type} #{dependency} is not defined in prior stages") + end end end diff --git a/lib/gitlab/ci/yaml_processor/dag.rb b/lib/gitlab/ci/yaml_processor/dag.rb new file mode 100644 index 00000000000..0140218d9bc --- /dev/null +++ b/lib/gitlab/ci/yaml_processor/dag.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Represents Dag pipeline +module Gitlab + module Ci + class YamlProcessor + class Dag + include TSort + + MissingNodeError = Class.new(StandardError) + + def initialize(nodes) + @nodes = nodes + end + + def self.check_circular_dependencies!(jobs) + nodes = jobs.values.to_h do |job| + name = job[:name].to_s + needs = job.dig(:needs, :job).to_a + + [name, needs.map { |need| need[:name].to_s }] + end + + new(nodes).tsort + rescue TSort::Cyclic + raise ValidationError, 'The pipeline has circular dependencies.' + rescue MissingNodeError + end + + def tsort_each_child(node, &block) + raise MissingNodeError, "node #{node} is missing" unless @nodes[node] + + @nodes[node].each(&block) + end + + def tsort_each_node(&block) + @nodes.each_key(&block) + end + end + end + end +end diff --git a/lib/sidebars/groups/context.rb b/lib/sidebars/groups/context.rb new file mode 100644 index 00000000000..6e0c6c1a2db --- /dev/null +++ b/lib/sidebars/groups/context.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Sidebars + module Groups + class Context < ::Sidebars::Context + def initialize(current_user:, container:, **args) + super(current_user: current_user, container: container, group: container, **args) + end + end + end +end diff --git a/lib/sidebars/groups/panel.rb b/lib/sidebars/groups/panel.rb new file mode 100644 index 00000000000..c11ca04c316 --- /dev/null +++ b/lib/sidebars/groups/panel.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Sidebars + module Groups + class Panel < ::Sidebars::Panel + override :render_raw_scope_menu_partial + def render_raw_scope_menu_partial + 'layouts/nav/sidebar/group_scope_menu' + end + + override :render_raw_menus_partial + def render_raw_menus_partial + 'layouts/nav/sidebar/group_menus' + end + + override :aria_label + def aria_label + context.group.subgroup? ? _('Subgroup navigation') : _('Group navigation') + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b439b7d869d..df97469d47d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39411,9 +39411,6 @@ msgstr "" msgid "mrWidget|You are not allowed to edit this project directly. Please fork to make changes." msgstr "" -msgid "mrWidget|You can delete the source branch now" -msgstr "" - msgid "mrWidget|You can merge after removing denied licenses" msgstr "" diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb index 9a3b129b6d2..338a135614d 100644 --- a/qa/qa/page/group/menu.rb +++ b/qa/qa/page/group/menu.rb @@ -6,7 +6,7 @@ module QA class Menu < Page::Base include SubMenus::Common - view 'app/views/layouts/nav/sidebar/_group.html.haml' do + view 'app/views/layouts/nav/sidebar/_group_menus.html.haml' do element :general_settings_link element :group_issues_item element :group_members_item diff --git a/qa/qa/page/group/sub_menus/common.rb b/qa/qa/page/group/sub_menus/common.rb index 86102f70d29..2f8a3fdeb4e 100644 --- a/qa/qa/page/group/sub_menus/common.rb +++ b/qa/qa/page/group/sub_menus/common.rb @@ -12,8 +12,8 @@ module QA super base.class_eval do - view 'app/views/layouts/nav/sidebar/_group.html.haml' do - element :group_sidebar + view 'app/views/shared/nav/_sidebar.html.haml' do + element :group_sidebar, 'qa_selector: sidebar_qa_selector(sidebar.container)' # rubocop:disable QA/ElementWithPattern end end end diff --git a/scripts/utils.sh b/scripts/utils.sh index 529491c3a0d..700dad58779 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -40,7 +40,7 @@ function bundle_install_script() { bundle config set path 'vendor' bundle config set clean 'true' - echo $BUNDLE_WITHOUT + echo "${BUNDLE_WITHOUT}" bundle config run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS} ${extra_install_args} && bundle check" @@ -134,3 +134,10 @@ function fail_pipeline_early() { scripts/api/cancel_pipeline.rb fi } + +function danger_as_local() { + # Force danger to skip CI source GitLab and fallback to "local only git repo". + unset GITLAB_CI + # We need to base SHA to help danger determine the base commit for this shallow clone. + bundle exec danger dry_run --fail-on-errors=true --verbose --base="${CI_MERGE_REQUEST_DIFF_BASE_SHA}" +} diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index e2ebd8b267d..59bf397adf2 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -269,10 +269,7 @@ RSpec.describe 'Admin updates settings' do end context 'Integrations page' do - let(:mailgun_events_receiver_enabled) { true } - before do - stub_feature_flags(mailgun_events_receiver: mailgun_events_receiver_enabled) visit general_admin_application_settings_path end @@ -286,26 +283,16 @@ RSpec.describe 'Admin updates settings' do expect(current_settings.hide_third_party_offers).to be true end - context 'when mailgun_events_receiver feature flag is enabled' do - it 'enabling Mailgun events', :aggregate_failures do - page.within('.as-mailgun') do - check 'Enable Mailgun event receiver' - fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY' - click_button 'Save changes' - end - - expect(page).to have_content 'Application settings saved successfully' - expect(current_settings.mailgun_events_enabled).to be true - expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY' + it 'enabling Mailgun events', :aggregate_failures do + page.within('.as-mailgun') do + check 'Enable Mailgun event receiver' + fill_in 'Mailgun HTTP webhook signing key', with: 'MAILGUN_SIGNING_KEY' + click_button 'Save changes' end - end - - context 'when mailgun_events_receiver feature flag is disabled' do - let(:mailgun_events_receiver_enabled) { false } - it 'does not have mailgun' do - expect(page).not_to have_selector('.as-mailgun') - end + expect(page).to have_content 'Application settings saved successfully' + expect(current_settings.mailgun_events_enabled).to be true + expect(current_settings.mailgun_signing_key).to eq 'MAILGUN_SIGNING_KEY' end end diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb index 999449a94b0..73fde7cafe5 100644 --- a/spec/features/groups/user_browse_projects_group_page_spec.rb +++ b/spec/features/groups/user_browse_projects_group_page_spec.rb @@ -23,7 +23,7 @@ RSpec.describe 'User browse group projects page' do visit projects_group_path(group) expect(page).to have_link project.name - expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived') + expect(page).to have_css('span.badge.badge-warning', text: 'archived') end end end diff --git a/spec/finders/container_repositories_finder_spec.rb b/spec/finders/container_repositories_finder_spec.rb index d247d61ecdb..5d449d1b811 100644 --- a/spec/finders/container_repositories_finder_spec.rb +++ b/spec/finders/container_repositories_finder_spec.rb @@ -7,12 +7,14 @@ RSpec.describe ContainerRepositoriesFinder do let_it_be(:guest) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, group: group) } + let_it_be(:project) { create(:project, :public, group: group) } let_it_be(:project_repository) { create(:container_repository, name: 'my_image', project: project) } let(:params) { {} } before do + project.project_feature.update!(container_registry_access_level: ProjectFeature::PRIVATE) + group.add_reporter(reporter) project.add_reporter(reporter) end @@ -77,6 +79,14 @@ RSpec.describe ContainerRepositoriesFinder do it_behaves_like 'with name search' it_behaves_like 'with sorting' + + context 'when project has container registry disabled' do + before do + project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) + end + + it { is_expected.to match_array([other_repository]) } + end end context 'when subject_type is project' do @@ -86,6 +96,14 @@ RSpec.describe ContainerRepositoriesFinder do it_behaves_like 'with name search' it_behaves_like 'with sorting' + + context 'when project has container registry disabled' do + before do + project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) + end + + it { is_expected.to be nil } + end end context 'with invalid subject_type' do @@ -96,9 +114,19 @@ RSpec.describe ContainerRepositoriesFinder do end context 'with unauthorized user' do - subject { described_class.new(user: guest, subject: group).execute } + subject { described_class.new(user: guest, subject: subject_type).execute } - it { is_expected.to be nil } + context 'when subject_type is group' do + let(:subject_type) { group } + + it { is_expected.to be nil } + end + + context 'when subject_type is project' do + let(:subject_type) { project } + + it { is_expected.to be nil } + end end end end diff --git a/spec/fixtures/error_tracking/event.txt b/spec/fixtures/error_tracking/event.txt new file mode 100644 index 00000000000..e87eb885e10 --- /dev/null +++ b/spec/fixtures/error_tracking/event.txt @@ -0,0 +1,3 @@ +{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","dsn":"http://1fedb514e17f4b958435093deb03048c@localhost:3000/api/v4/projects/7/error_tracking/collector/7","sdk":{"name":"sentry.ruby","version":"4.5.1"},"sent_at":"2021-07-08T12:59:16Z"} +{"type":"event","content_type":"application/json"} +{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","level":"error","timestamp":"2021-07-08T12:59:16Z","release":"db853d7","environment":"development","server_name":"MacBook.local","modules":{"rake":"13.0.3","concurrent-ruby":"1.1.9","i18n":"1.8.10","minitest":"5.14.4","thread_safe":"0.3.6","tzinfo":"1.2.9","uglifier":"4.2.0","web-console":"3.7.0"},"message":"","user":{},"tags":{"request_id":"4253dcd9-5e48-474a-89b4-0e945ab825af"},"contexts":{"os":{"name":"Darwin","version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64","build":"20.5.0","kernel_version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64"},"runtime":{"name":"ruby","version":"ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin19]"},"trace":{"trace_id":"d82b93fbc39e4d13b85762afa2e3ff36","span_id":"4a3ed8701e7f4ea4","parent_span_id":null,"description":null,"op":"rails.request","status":null}},"extra":{},"fingerprint":[],"breadcrumbs":{"values":[{"category":"start_processing.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.5553},"level":null,"message":"","timestamp":1625749156,"type":null},{"category":"process_action.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.55539,"view_runtime":null,"db_runtime":0},"level":null,"message":"","timestamp":1625749156,"type":null}]},"transaction":"PostsController#error2","platform":"ruby","sdk":{"name":"sentry.ruby.rails","version":"4.5.1"},"request":{"url":"http://localhost/posts/error2","method":"GET","headers":{},"env":{"SERVER_NAME":"localhost","SERVER_PORT":"4444"}},"exception":{"values":[{"type":"ActionView::MissingTemplate","value":"Missing template posts/error2, application/error2 with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:\n * \"/Users/developer/rails-project/app/views\"\n","module":"ActionView","thread_id":70254489510160,"stacktrace":{"frames":[{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/thread_pool.rb","function":"block in spawn_thread","lineno":135,"in_app":false,"filename":"puma/thread_pool.rb","pre_context":[" end\n","\n"," begin\n"],"context_line":" block.call(work, *extra)\n","post_context":[" rescue Exception => e\n"," STDERR.puts \"Error reached top of thread-pool: #{e.message} (#{e.class})\"\n"," end\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/server.rb","function":"block in run","lineno":334,"in_app":false,"filename":"puma/server.rb","pre_context":[" client.close\n"," else\n"," if process_now\n"],"context_line":" process_client client, buffer\n","post_context":[" else\n"," client.set_timeout @first_data_timeout\n"," @reactor.add client\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.6/lib/action_view/path_set.rb","function":"find","lineno":48,"in_app":false,"filename":"action_view/path_set.rb","pre_context":[" end\n","\n"," def find(*args)\n"],"context_line":" find_all(*args).first || raise(MissingTemplate.new(self, *args))\n","post_context":[" end\n","\n"," def find_file(path, prefixes = [], *args)\n"]}]}}]}}
\ No newline at end of file diff --git a/spec/fixtures/error_tracking/parsed_event.json b/spec/fixtures/error_tracking/parsed_event.json new file mode 100644 index 00000000000..1b144bd43dd --- /dev/null +++ b/spec/fixtures/error_tracking/parsed_event.json @@ -0,0 +1 @@ +{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","level":"error","timestamp":"2021-07-08T12:59:16Z","release":"db853d7","environment":"development","server_name":"MacBook.local","modules":{"rake":"13.0.3","concurrent-ruby":"1.1.9","i18n":"1.8.10","minitest":"5.14.4","thread_safe":"0.3.6","tzinfo":"1.2.9","uglifier":"4.2.0","web-console":"3.7.0"},"message":"","user":{},"tags":{"request_id":"4253dcd9-5e48-474a-89b4-0e945ab825af"},"contexts":{"os":{"name":"Darwin","version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64","build":"20.5.0","kernel_version":"Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64"},"runtime":{"name":"ruby","version":"ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin19]"},"trace":{"trace_id":"d82b93fbc39e4d13b85762afa2e3ff36","span_id":"4a3ed8701e7f4ea4","parent_span_id":null,"description":null,"op":"rails.request","status":null}},"extra":{},"fingerprint":[],"breadcrumbs":{"values":[{"category":"start_processing.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.5553},"level":null,"message":"","timestamp":1625749156,"type":null},{"category":"process_action.action_controller","data":{"controller":"PostsController","action":"error2","params":{"controller":"posts","action":"error2"},"format":"html","method":"GET","path":"/posts/error2","start_timestamp":1625749156.55539,"view_runtime":null,"db_runtime":0},"level":null,"message":"","timestamp":1625749156,"type":null}]},"transaction":"PostsController#error2","platform":"ruby","sdk":{"name":"sentry.ruby.rails","version":"4.5.1"},"request":{"url":"http://localhost/posts/error2","method":"GET","headers":{},"env":{"SERVER_NAME":"localhost","SERVER_PORT":"4444"}},"exception":{"values":[{"type":"ActionView::MissingTemplate","value":"Missing template posts/error2, application/error2 with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:\n * \"/Users/developer/rails-project/app/views\"\n","module":"ActionView","thread_id":70254489510160,"stacktrace":{"frames":[{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/thread_pool.rb","function":"block in spawn_thread","lineno":135,"in_app":false,"filename":"puma/thread_pool.rb","pre_context":[" end\n","\n"," begin\n"],"context_line":" block.call(work, *extra)\n","post_context":[" rescue Exception => e\n"," STDERR.puts \"Error reached top of thread-pool: #{e.message} (#{e.class})\"\n"," end\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/puma-3.12.6/lib/puma/server.rb","function":"block in run","lineno":334,"in_app":false,"filename":"puma/server.rb","pre_context":[" client.close\n"," else\n"," if process_now\n"],"context_line":" process_client client, buffer\n","post_context":[" else\n"," client.set_timeout @first_data_timeout\n"," @reactor.add client\n"]},{"project_root":"/Users/developer/rails-project","abs_path":"/Users/developer/.asdf/installs/ruby/2.5.1/lib/ruby/gems/2.5.0/gems/actionview-5.2.6/lib/action_view/path_set.rb","function":"find","lineno":48,"in_app":false,"filename":"action_view/path_set.rb","pre_context":[" end\n","\n"," def find(*args)\n"],"context_line":" find_all(*args).first || raise(MissingTemplate.new(self, *args))\n","post_context":[" end\n","\n"," def find_file(path, prefixes = [], *args)\n"]}]}}]}}
\ No newline at end of file diff --git a/spec/fixtures/error_tracking/transaction.txt b/spec/fixtures/error_tracking/transaction.txt new file mode 100644 index 00000000000..3d3f2aa90f0 --- /dev/null +++ b/spec/fixtures/error_tracking/transaction.txt @@ -0,0 +1,3 @@ +{"event_id":"4a304dbdf3404e87962e99bced2f6c8b","dsn":"","sdk":{"name":"sentry.ruby","version":"4.5.1"},"sent_at":"2021-07-08T12:58:29Z"} +{"type":"transaction","content_type":"application/json"} +{}
\ No newline at end of file diff --git a/spec/fixtures/error_tracking/unknown.txt b/spec/fixtures/error_tracking/unknown.txt new file mode 100644 index 00000000000..2a5c51f2596 --- /dev/null +++ b/spec/fixtures/error_tracking/unknown.txt @@ -0,0 +1,3 @@ +{"event_id":"7c9ae6e58f03442b9203bbdcf6ae904c","dsn":"","sdk":{"name":"sentry.ruby","version":"4.5.1"},"sent_at":"2021-07-08T12:59:16Z"} +{"type":"unknown","content_type":"application/json"} +{}
\ No newline at end of file diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index e427a029866..4c253f0610b 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -483,11 +483,11 @@ describe('Settings Panel', () => { it.each` visibilityLevel | pagesAccessControlForced | output ${visibilityOptions.PRIVATE} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [visibilityOptions.PUBLIC, 'Everyone']]} + ${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} ${visibilityOptions.INTERNAL} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [visibilityOptions.PUBLIC, 'Everyone']]} + ${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} ${visibilityOptions.PUBLIC} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [visibilityOptions.PUBLIC, 'Everyone']]} + ${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} `( 'renders correct options when pagesAccessControlForced is $pagesAccessControlForced and visibilityLevel is $visibilityLevel', async ({ visibilityLevel, pagesAccessControlForced, output }) => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js index 6bb87893c31..9c3a6d581e8 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -217,7 +217,6 @@ describe('MRWidgetMerged', () => { vm.mr.sourceBranchRemoved = false; Vue.nextTick(() => { - expect(vm.$el.innerText).toContain('You can delete the source branch now'); expect(vm.$el.innerText).not.toContain('The source branch has been deleted'); done(); }); @@ -229,7 +228,6 @@ describe('MRWidgetMerged', () => { Vue.nextTick(() => { expect(vm.$el.innerText).toContain('The source branch is being deleted'); - expect(vm.$el.innerText).not.toContain('You can delete the source branch now'); expect(vm.$el.innerText).not.toContain('The source branch has been deleted'); done(); }); diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb index 93d32cb8418..8b3c8411fbd 100644 --- a/spec/helpers/packages_helper_spec.rb +++ b/spec/helpers/packages_helper_spec.rb @@ -66,6 +66,7 @@ RSpec.describe PackagesHelper do end describe '#show_cleanup_policy_on_alert' do + let_it_be(:user) { create(:user) } let_it_be_with_reload(:container_repository) { create(:container_repository) } subject { helper.show_cleanup_policy_on_alert(project.reload) } @@ -203,9 +204,10 @@ RSpec.describe PackagesHelper do with_them do before do + allow(helper).to receive(:current_user).and_return(user) allow(Gitlab).to receive(:com?).and_return(com) stub_config(registry: { enabled: config_registry }) - allow(project).to receive(:container_registry_enabled).and_return(project_registry) + allow(project).to receive(:feature_available?).with(:container_registry, user).and_return(project_registry) stub_application_setting(container_expiration_policies_enable_historic_entries: historic_entries) stub_feature_flags(container_expiration_policies_historic_entry: false) stub_feature_flags(container_expiration_policies_historic_entry: project) if historic_entry diff --git a/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb b/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb new file mode 100644 index 00000000000..6f12c6d25e0 --- /dev/null +++ b/spec/lib/error_tracking/collector/sentry_request_parser_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ErrorTracking::Collector::SentryRequestParser do + describe '.parse' do + let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') } + let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) } + + let(:body) { raw_event } + let(:headers) { { 'Content-Encoding' => '' } } + let(:request) { double('request', headers: headers, body: StringIO.new(body)) } + + subject { described_class.parse(request) } + + RSpec.shared_examples 'valid parser' do + it 'returns a valid hash' do + parsed_request = subject + + expect(parsed_request[:request_type]).to eq('event') + expect(parsed_request[:event]).to eq(parsed_event) + end + end + + context 'empty body content' do + let(:body) { '' } + + it 'fails with exception' do + expect { subject }.to raise_error(StandardError) + end + end + + context 'plain text sentry request' do + it_behaves_like 'valid parser' + end + + context 'gzip encoded sentry request' do + let(:headers) { { 'Content-Encoding' => 'gzip' } } + let(:body) { Zlib.gzip(raw_event) } + + it_behaves_like 'valid parser' + end + end +end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index aaa3a7a8b9d..77f6608eb85 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -247,7 +247,7 @@ RSpec.describe Gitlab::Ci::Lint do include_context 'advanced validations' do it 'runs advanced logical validations' do expect(subject).not_to be_valid - expect(subject.errors).to eq(["'test' job needs 'build' job, but it was not added to the pipeline"]) + expect(subject.errors).to eq(["'test' job needs 'build' job, but 'build' is not in any previous stage"]) end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index f6c456e488d..42878b508de 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -11,8 +11,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:seed_context) { double(pipeline: pipeline, root_variables: root_variables) } let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage } } let(:previous_stages) { [] } + let(:current_stage) { double(seeds_names: [attributes[:name]]) } - let(:seed_build) { described_class.new(seed_context, attributes, previous_stages) } + let(:seed_build) { described_class.new(seed_context, attributes, previous_stages, current_stage) } describe '#attributes' do subject { seed_build.attributes } @@ -1079,7 +1080,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it "returns an error" do expect(subject.errors).to contain_exactly( - "'rspec' job needs 'build' job, but it was not added to the pipeline") + "'rspec' job needs 'build' job, but 'build' is not in any previous stage") end context 'when the needed job is optional' do @@ -1115,6 +1116,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end end + context 'when build job is part of the same stage' do + let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) } + + it 'is included' do + is_expected.to be_included + end + + it 'does not have errors' do + expect(subject.errors).to be_empty + end + + context 'when ci_same_stage_job_needs FF is disabled' do + before do + stub_feature_flags(ci_same_stage_job_needs: false) + end + + it 'has errors' do + expect(subject.errors).to contain_exactly("'rspec' job needs 'build' job, but 'build' is not in any previous stage") + end + end + end + context 'when using 101 needs' do let(:needs_count) { 101 } diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb index 21be8660def..3424e7d03a3 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb @@ -34,6 +34,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do described_class.new(seed_context, stages_attributes) end + before do + stub_feature_flags(ci_same_stage_job_needs: false) + end + describe '#stages' do it 'returns the stage resources' do stages = seed.stages @@ -65,7 +69,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do } expect(seed.errors).to contain_exactly( - "'invalid_job' job needs 'non-existent' job, but it was not added to the pipeline") + "'invalid_job' job needs 'non-existent' job, but 'non-existent' is not in any previous stage") end end end diff --git a/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb new file mode 100644 index 00000000000..af1b43f6b01 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor/dag_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::YamlProcessor::Dag do + let(:nodes) { {} } + + subject(:result) { described_class.new(nodes).tsort } + + context 'when it is a regular pipeline' do + let(:nodes) do + { 'job_c' => %w(job_b job_d), 'job_d' => %w(job_a), 'job_b' => %w(job_a), 'job_a' => %w() } + end + + it 'returns ordered jobs' do + expect(result).to eq(%w(job_a job_b job_d job_c)) + end + end + + context 'when there is a circular dependency' do + let(:nodes) do + { 'job_a' => %w(job_c), 'job_b' => %w(job_a), 'job_c' => %w(job_b) } + end + + it 'raises TSort::Cyclic' do + expect { result }.to raise_error(TSort::Cyclic, /topological sort failed/) + end + end + + context 'when there is a missing job' do + let(:nodes) do + { 'job_a' => %w(job_d), 'job_b' => %w(job_a) } + end + + it 'raises MissingNodeError' do + expect { result }.to raise_error( + Gitlab::Ci::YamlProcessor::Dag::MissingNodeError, 'node job_d is missing' + ) + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index dccf8c0695d..19c2e34a0f0 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -595,7 +595,15 @@ module Gitlab EOYML end - it_behaves_like 'has warnings and expected error', /build job: need test is not defined in prior stages/ + it_behaves_like 'has warnings and expected error', /build job: need test is not defined in current or prior stages/ + + context 'with ci_same_stage_job_needs FF disabled' do + before do + stub_feature_flags(ci_same_stage_job_needs: false) + end + + it_behaves_like 'has warnings and expected error', /build job: need test is not defined in prior stages/ + end end end end @@ -1858,7 +1866,7 @@ module Gitlab build2: { stage: 'build', script: 'test' }, test1: { stage: 'test', script: 'test', dependencies: dependencies }, test2: { stage: 'test', script: 'test' }, - deploy: { stage: 'test', script: 'test' } + deploy: { stage: 'deploy', script: 'test' } } end @@ -1891,7 +1899,15 @@ module Gitlab context 'dependencies to deploy' do let(:dependencies) { ['deploy'] } - it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in prior stages' + it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in current or prior stages' + + context 'with ci_same_stage_job_needs FF disabled' do + before do + stub_feature_flags(ci_same_stage_job_needs: false) + end + + it_behaves_like 'returns errors', 'test1 job: dependency deploy is not defined in prior stages' + end end context 'when a job depends on another job that references a not-yet defined stage' do @@ -1916,7 +1932,7 @@ module Gitlab } end - it_behaves_like 'returns errors', /is not defined in prior stages/ + it_behaves_like 'returns errors', /is not defined in current or prior stages/ end end @@ -1931,7 +1947,7 @@ module Gitlab parallel: { stage: 'build', script: 'test', parallel: 2 }, test1: { stage: 'test', script: 'test', needs: needs, dependencies: dependencies }, test2: { stage: 'test', script: 'test' }, - deploy: { stage: 'test', script: 'test' } + deploy: { stage: 'deploy', script: 'test' } } end @@ -1941,6 +1957,45 @@ module Gitlab it { is_expected.to be_valid } end + context 'needs a job from the same stage' do + let(:needs) { %w(test2) } + + it 'creates jobs with valid specifications' do + expect(subject.builds.size).to eq(7) + expect(subject.builds[0]).to eq( + stage: 'build', + stage_idx: 1, + name: 'build1', + only: { refs: %w[branches tags] }, + options: { + script: ['test'] + }, + when: 'on_success', + allow_failure: false, + yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :stage + ) + expect(subject.builds[4]).to eq( + stage: 'test', + stage_idx: 2, + name: 'test1', + only: { refs: %w[branches tags] }, + options: { script: ['test'] }, + needs_attributes: [ + { name: 'test2', artifacts: true, optional: false } + ], + when: 'on_success', + allow_failure: false, + yaml_variables: [], + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :dag + ) + end + end + context 'needs two builds' do let(:needs) { %w(build1 build2) } @@ -2096,7 +2151,15 @@ module Gitlab context 'needs to deploy' do let(:needs) { ['deploy'] } - it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in prior stages' + it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in current or prior stages' + + context 'with ci_same_stage_job_needs FF disabled' do + before do + stub_feature_flags(ci_same_stage_job_needs: false) + end + + it_behaves_like 'returns errors', 'test1 job: need deploy is not defined in prior stages' + end end context 'needs and dependencies that are mismatching' do @@ -2767,6 +2830,29 @@ module Gitlab it_behaves_like 'returns errors', 'jobs:rspec:parallel should be an integer or a hash' end + + context 'when the pipeline has a circular dependency' do + let(:config) do + <<~YAML + job_a: + stage: test + script: build + needs: [job_c] + + job_b: + stage: test + script: test + needs: [job_a] + + job_c: + stage: test + script: deploy + needs: [job_b] + YAML + end + + it_behaves_like 'returns errors', 'The pipeline has circular dependencies.' + end end describe '#execute' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 93e931579ed..78805cea66a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -578,6 +578,7 @@ project: - merge_request_metrics - security_orchestration_policy_configuration - timelogs +- error_tracking_errors award_emoji: - awardable - user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 77d126e012e..10162ade48b 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -167,6 +167,7 @@ ProjectMember: - expires_at - ldap - override +- invite_email_success User: - id - username diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ae956adf563..64fb10d1556 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -827,15 +827,15 @@ RSpec.describe Notify do end end - context 'when on gitlab.com' do + context 'when mailgun events are enabled' do before do - allow(Gitlab).to receive(:dev_env_or_com?).and_return(true) + stub_application_setting(mailgun_events_enabled: true) end it 'has custom headers' do aggregate_failures do - expect(subject).to have_header('X-Mailgun-Tag', 'invite_email') - expect(subject).to have_header('X-Mailgun-Variables', { 'invite_token' => project_member.invite_token }.to_json) + expect(subject).to have_header('X-Mailgun-Tag', ::Members::Mailgun::INVITE_EMAIL_TAG) + expect(subject).to have_header('X-Mailgun-Variables', { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => project_member.invite_token }.to_json) end end end diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb index 331ba9953ca..cd330324840 100644 --- a/spec/models/ci/build_dependencies_spec.rb +++ b/spec/models/ci/build_dependencies_spec.rb @@ -55,6 +55,24 @@ RSpec.describe Ci::BuildDependencies do end end end + + context 'when needs refer to jobs from the same stage' do + let(:job) do + create(:ci_build, + pipeline: pipeline, + name: 'dag_job', + scheduling_type: :dag, + stage_idx: 2, + stage: 'deploy' + ) + end + + before do + create(:ci_build_need, build: job, name: 'staging', artifacts: true) + end + + it { is_expected.to contain_exactly(staging) } + end end describe 'jobs from specified dependencies' do diff --git a/spec/requests/api/error_tracking_collector_spec.rb b/spec/requests/api/error_tracking_collector_spec.rb new file mode 100644 index 00000000000..52d63410e7a --- /dev/null +++ b/spec/requests/api/error_tracking_collector_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ErrorTrackingCollector do + let_it_be(:project) { create(:project, :private) } + let_it_be(:setting) { create(:project_error_tracking_setting, project: project) } + + describe "POST /error_tracking/collector/api/:id/envelope" do + let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') } + let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/envelope" } + + let(:params) { raw_event } + + subject { post api(url), params: params } + + RSpec.shared_examples 'not found' do + it 'reponds with 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + RSpec.shared_examples 'bad request' do + it 'responds with 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'error tracking feature is disabled' do + before do + setting.update!(enabled: false) + end + + it_behaves_like 'not found' + end + + context 'feature flag is disabled' do + before do + stub_feature_flags(integrated_error_tracking: false) + end + + it_behaves_like 'not found' + end + + context 'empty body' do + let(:params) { '' } + + it_behaves_like 'bad request' + end + + context 'unknown request type' do + let(:params) { fixture_file('error_tracking/unknown.txt') } + + it_behaves_like 'bad request' + end + + context 'transaction request type' do + let(:params) { fixture_file('error_tracking/transaction.txt') } + + it 'does nothing and returns no content' do + expect { subject }.not_to change { ErrorTracking::ErrorEvent.count } + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + it 'writes to the database and returns no content' do + expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1) + + expect(response).to have_gitlab_http_status(:no_content) + end + end +end diff --git a/spec/requests/members/mailgun/permanent_failure_spec.rb b/spec/requests/members/mailgun/permanent_failure_spec.rb new file mode 100644 index 00000000000..e47aedf8e94 --- /dev/null +++ b/spec/requests/members/mailgun/permanent_failure_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'receive a permanent failure' do + describe 'POST /members/mailgun/permanent_failures', :aggregate_failures do + let_it_be(:member) { create(:project_member, :invited) } + + let(:raw_invite_token) { member.raw_invite_token } + let(:mailgun_events) { true } + let(:mailgun_signing_key) { 'abc123' } + + subject(:post_request) { post members_mailgun_permanent_failures_path(standard_params) } + + before do + stub_application_setting(mailgun_events_enabled: mailgun_events, mailgun_signing_key: mailgun_signing_key) + end + + it 'marks the member invite email success as false' do + expect { post_request }.to change { member.reload.invite_email_success }.from(true).to(false) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the change to a member is not made' do + context 'with incorrect signing key' do + context 'with incorrect signing key' do + let(:mailgun_signing_key) { '_foobar_' } + + it 'does not change member status and responds as not_found' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with nil signing key' do + let(:mailgun_signing_key) { nil } + + it 'does not change member status and responds as not_found' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when the feature is not enabled' do + let(:mailgun_events) { false } + + it 'does not change member status and responds as expected' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_acceptable) + end + end + + context 'when it is not an invite email' do + before do + stub_const('::Members::Mailgun::INVITE_EMAIL_TAG', '_foobar_') + end + + it 'does not change member status and responds as expected' do + expect { post_request }.not_to change { member.reload.invite_email_success } + + expect(response).to have_gitlab_http_status(:not_acceptable) + end + end + end + + def standard_params + { + "signature": { + "timestamp": "1625056677", + "token": "eb944d0ace7227667a1b97d2d07276ae51d2b849ed2cfa68f3", + "signature": "9790cc6686eb70f0b1f869180d906870cdfd496d27fee81da0aa86b9e539e790" + }, + "event-data": { + "severity": "permanent", + "tags": ["invite_email"], + "timestamp": 1521233195.375624, + "storage": { + "url": "_anything_", + "key": "_anything_" + }, + "log-level": "error", + "id": "_anything_", + "campaigns": [], + "reason": "suppress-bounce", + "user-variables": { + "invite_token": raw_invite_token + }, + "flags": { + "is-routed": false, + "is-authenticated": true, + "is-system-test": false, + "is-test-mode": false + }, + "recipient-domain": "example.com", + "envelope": { + "sender": "bob@mg.gitlab.com", + "transport": "smtp", + "targets": "alice@example.com" + }, + "message": { + "headers": { + "to": "Alice <alice@example.com>", + "message-id": "20130503192659.13651.20287@mg.gitlab.com", + "from": "Bob <bob@mg.gitlab.com>", + "subject": "Test permanent_fail webhook" + }, + "attachments": [], + "size": 111 + }, + "recipient": "alice@example.com", + "event": "failed", + "delivery-status": { + "attempt-no": 1, + "message": "", + "code": 605, + "description": "Not delivering to previously bounced address", + "session-seconds": 0 + } + } + } + end + end +end diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/after_requeue_job_service_spec.rb index a2147759dba..f8c49060ce0 100644 --- a/spec/services/ci/after_requeue_job_service_spec.rb +++ b/spec/services/ci/after_requeue_job_service_spec.rb @@ -8,9 +8,9 @@ RSpec.describe Ci::AfterRequeueJobService do let(:pipeline) { create(:ci_pipeline, project: project) } - let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } let!(:test1) { create(:ci_build, :success, pipeline: pipeline, stage_idx: 1) } let!(:test2) { create(:ci_build, :skipped, pipeline: pipeline, stage_idx: 1) } + let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 0, name: 'build') } subject(:execute_service) { described_class.new(project, user).execute(build) } @@ -24,6 +24,34 @@ RSpec.describe Ci::AfterRequeueJobService do expect(test2.reload).to be_created end + context 'when there is a job need from the same stage' do + let!(:test3) do + create(:ci_build, + :skipped, + pipeline: pipeline, + stage_idx: 0, + scheduling_type: :dag) + end + + before do + create(:ci_build_need, build: test3, name: 'build') + end + + it 'marks subsequent skipped jobs as processable' do + expect { execute_service }.to change { test3.reload.status }.from('skipped').to('created') + end + + context 'with ci_same_stage_job_needs FF disabled' do + before do + stub_feature_flags(ci_same_stage_job_needs: false) + end + + it 'does nothing with the build' do + expect { execute_service }.not_to change { test3.reload.status } + end + end + end + context 'when the pipeline is a downstream pipeline and the bridge is depended' do let!(:trigger_job) { create(:ci_bridge, :strategy_depend, status: 'success') } diff --git a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb index 7193e5bd7d4..a42770aae20 100644 --- a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb +++ b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Ci::CreatePipelineService do end it 'contains both errors and warnings' do - error_message = 'build job: need test is not defined in prior stages' + error_message = 'build job: need test is not defined in current or prior stages' warning_message = /jobs:test may allow multiple pipelines to run/ expect(pipeline.yaml_errors).to eq(error_message) diff --git a/spec/services/ci/create_pipeline_service/dry_run_spec.rb b/spec/services/ci/create_pipeline_service/dry_run_spec.rb index 0fb500f5729..01df7772eef 100644 --- a/spec/services/ci/create_pipeline_service/dry_run_spec.rb +++ b/spec/services/ci/create_pipeline_service/dry_run_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Ci::CreatePipelineService do it_behaves_like 'returns a non persisted pipeline' it 'returns a pipeline with errors', :aggregate_failures do - error_message = 'build job: need test is not defined in prior stages' + error_message = 'build job: need test is not defined in current or prior stages' expect(subject.error_messages.map(&:content)).to eq([error_message]) expect(subject.errors).not_to be_empty @@ -109,7 +109,7 @@ RSpec.describe Ci::CreatePipelineService do it_behaves_like 'returns a non persisted pipeline' it 'returns a pipeline with errors', :aggregate_failures do - error_message = "'test' job needs 'build' job, but it was not added to the pipeline" + error_message = "'test' job needs 'build' job, but 'build' is not in any previous stage" expect(subject.error_messages.map(&:content)).to eq([error_message]) expect(subject.errors).not_to be_empty diff --git a/spec/services/ci/create_pipeline_service/needs_spec.rb b/spec/services/ci/create_pipeline_service/needs_spec.rb index 3246a39e88b..d096db10d0b 100644 --- a/spec/services/ci/create_pipeline_service/needs_spec.rb +++ b/spec/services/ci/create_pipeline_service/needs_spec.rb @@ -257,7 +257,7 @@ RSpec.describe Ci::CreatePipelineService do it 'returns error' do expect(pipeline.yaml_errors) - .to eq("'test' job needs 'build' job, but it was not added to the pipeline") + .to eq("'test' job needs 'build' job, but 'build' is not in any previous stage") end context 'when need is optional' do diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb index 1164d344a79..7a6535ed3fa 100644 --- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb @@ -252,7 +252,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do end it_behaves_like 'creation failure' do - let(:expected_error) { /test job: dependency generator is not defined in prior stages/ } + let(:expected_error) { /test job: dependency generator is not defined in current or prior stages/ } end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index c27088f805f..64e8c6ac2df 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1715,7 +1715,7 @@ RSpec.describe Ci::CreatePipelineService do it 'contains the expected errors' do expect(pipeline.builds).to be_empty - error_message = "'test_a' job needs 'build_a' job, but it was not added to the pipeline" + error_message = "'test_a' job needs 'build_a' job, but 'build_a' is not in any previous stage" expect(pipeline.yaml_errors).to eq(error_message) expect(pipeline.error_messages.map(&:content)).to contain_exactly(error_message) expect(pipeline.errors[:base]).to contain_exactly(error_message) diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_same_stages.yml b/spec/services/ci/pipeline_processing/test_cases/dag_same_stages.yml new file mode 100644 index 00000000000..2a63daeb561 --- /dev/null +++ b/spec/services/ci/pipeline_processing/test_cases/dag_same_stages.yml @@ -0,0 +1,47 @@ +config: + build: + stage: test + script: exit 0 + + test: + stage: test + script: exit 0 + needs: [build] + + deploy: + stage: test + script: exit 0 + needs: [test] + +init: + expect: + pipeline: pending + stages: + test: pending + jobs: + build: pending + test: created + deploy: created + +transitions: + - event: success + jobs: [build] + expect: + pipeline: running + stages: + test: running + jobs: + build: success + test: pending + deploy: created + + - event: success + jobs: [test] + expect: + pipeline: running + stages: + test: running + jobs: + build: success + test: success + deploy: pending diff --git a/spec/services/error_tracking/collect_error_service_spec.rb b/spec/services/error_tracking/collect_error_service_spec.rb new file mode 100644 index 00000000000..14cd588f40b --- /dev/null +++ b/spec/services/error_tracking/collect_error_service_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ErrorTracking::CollectErrorService do + let_it_be(:project) { create(:project) } + let_it_be(:parsed_event) { Gitlab::Json.parse(fixture_file('error_tracking/parsed_event.json')) } + + subject { described_class.new(project, nil, event: parsed_event) } + + describe '#execute' do + it 'creates Error and creates ErrorEvent' do + expect { subject.execute } + .to change { ErrorTracking::Error.count }.by(1) + .and change { ErrorTracking::ErrorEvent.count }.by(1) + end + + it 'updates Error and created ErrorEvent on second hit' do + subject.execute + + expect { subject.execute }.not_to change { ErrorTracking::Error.count } + expect { subject.execute }.to change { ErrorTracking::ErrorEvent.count }.by(1) + end + + it 'has correct values set' do + subject.execute + + event = ErrorTracking::ErrorEvent.last + error = event.error + + expect(error.name).to eq 'ActionView::MissingTemplate' + expect(error.description).to start_with 'Missing template posts/error2' + expect(error.actor).to eq 'PostsController#error2' + expect(error.platform).to eq 'ruby' + expect(error.last_seen_at).to eq '2021-07-08T12:59:16Z' + + expect(event.description).to eq 'ActionView::MissingTemplate' + expect(event.occurred_at).to eq '2021-07-08T12:59:16Z' + expect(event.level).to eq 'error' + expect(event.environment).to eq 'development' + expect(event.payload).to eq parsed_event + end + end +end diff --git a/spec/services/members/mailgun/process_webhook_service_spec.rb b/spec/services/members/mailgun/process_webhook_service_spec.rb new file mode 100644 index 00000000000..d6a21183395 --- /dev/null +++ b/spec/services/members/mailgun/process_webhook_service_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Members::Mailgun::ProcessWebhookService do + describe '#execute', :aggregate_failures do + let_it_be(:member) { create(:project_member, :invited) } + + let(:raw_invite_token) { member.raw_invite_token } + let(:payload) { { 'user-variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => raw_invite_token } } } + + subject(:service) { described_class.new(payload).execute } + + it 'marks the member invite email success as false' do + expect(Gitlab::AppLogger).to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/).and_call_original + + expect { service }.to change { member.reload.invite_email_success }.from(true).to(false) + end + + context 'when member can not be found' do + let(:raw_invite_token) { '_foobar_' } + + it 'does not change member status' do + expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) + + expect { service }.not_to change { member.reload.invite_email_success } + end + end + + context 'when invite token is not found in payload' do + let(:payload) { {} } + + it 'does not change member status and logs an error' do + expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + an_instance_of(described_class::ProcessWebhookServiceError)) + + expect { service }.not_to change { member.reload.invite_email_success } + end + end + end +end |