summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml11
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue29
-rw-r--r--app/controllers/members/mailgun/permanent_failures_controller.rb65
-rw-r--r--app/finders/container_repositories_finder.rb2
-rw-r--r--app/helpers/packages_helper.rb2
-rw-r--r--app/helpers/sidebars_helper.rb13
-rw-r--r--app/mailers/emails/members.rb6
-rw-r--r--app/models/ci/build_dependencies.rb14
-rw-r--r--app/models/error_tracking/error.rb11
-rw-r--r--app/models/project.rb2
-rw-r--r--app/services/ci/after_requeue_job_service.rb24
-rw-r--r--app/services/error_tracking/collect_error_service.rb38
-rw-r--r--app/services/members/mailgun.rb8
-rw-r--r--app/services/members/mailgun/process_webhook_service.rb39
-rw-r--r--app/views/admin/application_settings/_mailgun.html.haml2
-rw-r--r--app/views/clusters/clusters/_multiple_clusters_message.html.haml2
-rw-r--r--app/views/groups/_delete_project_button.html.haml1
-rw-r--r--app/views/groups/_project_badges.html.haml2
-rw-r--r--app/views/groups/projects.html.haml5
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml179
-rw-r--r--app/views/layouts/nav/sidebar/_group_menus.html.haml166
-rw-r--r--app/views/layouts/nav/sidebar/_group_scope_menu.html.haml6
-rw-r--r--app/views/shared/nav/_sidebar.html.haml9
-rw-r--r--config/feature_flags/development/ci_same_stage_job_needs.yml8
-rw-r--r--config/feature_flags/development/integrated_error_tracking.yml8
-rw-r--r--config/feature_flags/development/load_balancing_for_update_all_mirrors_worker.yml8
-rw-r--r--config/feature_flags/development/mailgun_events_receiver.yml8
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/members.rb7
-rw-r--r--db/migrate/20210630144339_add_invite_email_success_to_members.rb7
-rw-r--r--db/migrate/20210713135152_add_devops_adoption_vulnerability_management_used_count.rb7
-rw-r--r--db/migrate/20210713144637_add_vulnerabilities_created_at_index.rb17
-rw-r--r--db/schema_migrations/202106301443391
-rw-r--r--db/schema_migrations/202107131351521
-rw-r--r--db/schema_migrations/202107131446371
-rw-r--r--db/structure.sql6
-rw-r--r--doc/administration/integration/mailgun.md41
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/ci/yaml/index.md24
-rw-r--r--doc/integration/elasticsearch.md5
-rw-r--r--doc/topics/autodevops/index.md55
-rw-r--r--doc/topics/autodevops/quick_start_guide.md2
-rw-r--r--doc/user/admin_area/settings/index.md1
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/error_tracking_collector.rb73
-rw-r--r--lib/error_tracking/collector/sentry_request_parser.rb38
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb21
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb18
-rw-r--r--lib/gitlab/ci/yaml_processor/dag.rb42
-rw-r--r--lib/sidebars/groups/context.rb11
-rw-r--r--lib/sidebars/groups/panel.rb22
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/page/group/menu.rb2
-rw-r--r--qa/qa/page/group/sub_menus/common.rb4
-rw-r--r--scripts/utils.sh9
-rw-r--r--spec/features/admin/admin_settings_spec.rb29
-rw-r--r--spec/features/groups/user_browse_projects_group_page_spec.rb2
-rw-r--r--spec/finders/container_repositories_finder_spec.rb34
-rw-r--r--spec/fixtures/error_tracking/event.txt3
-rw-r--r--spec/fixtures/error_tracking/parsed_event.json1
-rw-r--r--spec/fixtures/error_tracking/transaction.txt3
-rw-r--r--spec/fixtures/error_tracking/unknown.txt3
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js2
-rw-r--r--spec/helpers/packages_helper_spec.rb4
-rw-r--r--spec/lib/error_tracking/collector/sentry_request_parser_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/dag_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb98
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/mailers/notify_spec.rb8
-rw-r--r--spec/models/ci/build_dependencies_spec.rb18
-rw-r--r--spec/requests/api/error_tracking_collector_spec.rb77
-rw-r--r--spec/requests/members/mailgun/permanent_failure_spec.rb128
-rw-r--r--spec/services/ci/after_requeue_job_service_spec.rb30
-rw-r--r--spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/dry_run_spec.rb4
-rw-r--r--spec/services/ci/create_pipeline_service/needs_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_same_stages.yml47
-rw-r--r--spec/services/error_tracking/collect_error_service_spec.rb44
-rw-r--r--spec/services/members/mailgun/process_webhook_service_spec.rb42
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