Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-27 21:09:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-27 21:09:21 +0300
commite0fa0638a422c3e20d4423c9bb69d79afc9c7d3d (patch)
tree9abb3c0706576bbda895fe9539a55556930606e2
parentf8d15ca65390475e356b06dedc51e10ccd179f86 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.haml-lint_todo.yml2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue7
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue10
-rw-r--r--app/assets/javascripts/profile/gl_crop.js10
-rw-r--r--app/assets/stylesheets/utilities.scss1
-rw-r--r--app/mailers/emails/pipelines.rb4
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/ci/ref.rb23
-rw-r--r--app/models/notification_recipient.rb11
-rw-r--r--app/models/notification_setting.rb14
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/project_services/pipelines_email_service.rb2
-rw-r--r--app/services/ci/update_ci_ref_status_service.rb65
-rw-r--r--app/services/merge_requests/create_pipeline_service.rb23
-rw-r--r--app/services/notification_service.rb7
-rw-r--r--app/views/notify/_successful_pipeline.html.haml118
-rw-r--r--app/views/notify/_successful_pipeline.text.erb32
-rw-r--r--app/views/notify/pipeline_fixed_email.html.haml1
-rw-r--r--app/views/notify/pipeline_fixed_email.text.erb1
-rw-r--r--app/views/notify/pipeline_success_email.html.haml118
-rw-r--r--app/views/notify/pipeline_success_email.text.erb33
-rw-r--r--app/workers/all_queues.yml7
-rw-r--r--app/workers/pipeline_notification_worker.rb14
-rw-r--r--app/workers/pipeline_update_ci_ref_status_worker.rb17
-rw-r--r--changelogs/unreleased/11821-insights-back-json-fix.yml5
-rw-r--r--changelogs/unreleased/207455-frontend-fix-epic-blform.yml5
-rw-r--r--changelogs/unreleased/24309-notifications-for-when-pipelines-are-fixed.yml5
-rw-r--r--changelogs/unreleased/jdb-display-base-label-versions-dropdown.yml5
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--db/migrate/20191001170300_create_ci_ref.rb19
-rw-r--r--db/migrate/20191111165017_add_fixed_pipeline_to_notification_settings.rb9
-rw-r--r--db/migrate/20200226162156_rename_closed_at_to_dismissed_at_in_vulnerabilities.rb17
-rw-r--r--db/migrate/20200226162634_rename_closed_by_to_dismissed_by_in_vulnerabilities.rb17
-rw-r--r--db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb4
-rw-r--r--db/post_migrate/20200214173000_cleanup_empty_epic_user_mentions.rb30
-rw-r--r--db/post_migrate/20200214174519_remigrate_epic_mentions_to_db.rb38
-rw-r--r--db/post_migrate/20200214174607_remigrate_epic_notes_mentions_to_db.rb45
-rw-r--r--db/post_migrate/20200226162239_cleanup_closed_at_rename_in_vulnerabilities.rb17
-rw-r--r--db/post_migrate/20200226162723_cleanup_closed_by_rename_in_vulnerabilities.rb17
-rw-r--r--db/schema.rb24
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql21
-rw-r--r--doc/api/graphql/reference/index.md23
-rw-r--r--doc/api/notification_settings.md4
-rw-r--r--doc/integration/elasticsearch.md6
-rw-r--r--doc/user/application_security/license_compliance/index.md2
-rw-r--r--doc/user/profile/notifications.md3
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/epic.rb1
-rw-r--r--lib/gitlab/checks/snippet_check.rb2
-rw-r--r--lib/gitlab/email/receiver.rb8
-rw-r--r--lib/gitlab/git_access_snippet.rb8
-rw-r--r--lib/gitlab/graphql/docs/helper.rb22
-rw-r--r--lib/gitlab/graphql/docs/templates/default.md.haml5
-rw-r--r--lib/gitlab/incoming_email.rb8
-rw-r--r--lib/gitlab/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb13
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb104
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/server.rb13
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb21
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing.rb36
-rw-r--r--locale/gitlab.pot46
-rw-r--r--locale/unfound_translations.rb1
-rw-r--r--spec/factories/ci/ref.rb16
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb34
-rw-r--r--spec/frontend/diffs/components/compare_versions_dropdown_spec.js (renamed from spec/javascripts/diffs/components/compare_versions_dropdown_spec.js)19
-rw-r--r--spec/helpers/notifications_helper_spec.rb1
-rw-r--r--spec/lib/gitlab/checks/snippet_check_spec.rb4
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb22
-rw-r--r--spec/lib/gitlab/graphql/docs/renderer_spec.rb96
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb11
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb34
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb125
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb52
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing_spec.rb38
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb15
-rw-r--r--spec/lib/gitlab/sidekiq_middleware_spec.rb6
-rw-r--r--spec/mailers/emails/pipelines_spec.rb13
-rw-r--r--spec/models/ci/ref_spec.rb11
-rw-r--r--spec/models/notification_recipient_spec.rb42
-rw-r--r--spec/models/notification_setting_spec.rb3
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/services/ci/update_ci_ref_status_service_spec.rb161
-rw-r--r--spec/services/notification_service_spec.rb81
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/views/pipeline_status_changes_email.rb78
-rw-r--r--spec/views/notify/pipeline_failed_email.html.haml_spec.rb70
-rw-r--r--spec/views/notify/pipeline_fixed_email.html.haml_spec.rb10
-rw-r--r--spec/views/notify/pipeline_fixed_email.text.erb_spec.rb10
-rw-r--r--spec/views/notify/pipeline_success_email.html.haml_spec.rb54
-rw-r--r--spec/views/notify/pipeline_success_email.text.erb_spec.rb22
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb9
-rw-r--r--spec/workers/pipeline_update_ci_ref_status_worker_service_spec.rb18
94 files changed, 1826 insertions, 390 deletions
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index 2e1b1770f99..900dae7a3ab 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -146,6 +146,7 @@ linters:
- 'app/views/notify/_failed_builds.html.haml'
- 'app/views/notify/_reassigned_issuable_email.html.haml'
- 'app/views/notify/_removal_notification.html.haml'
+ - 'app/views/notify/_successful_pipeline.html.haml'
- 'app/views/notify/autodevops_disabled_email.html.haml'
- 'app/views/notify/changed_milestone_email.html.haml'
- 'app/views/notify/import_issues_csv_email.html.haml'
@@ -163,7 +164,6 @@ linters:
- 'app/views/notify/pages_domain_verification_failed_email.html.haml'
- 'app/views/notify/pages_domain_verification_succeeded_email.html.haml'
- 'app/views/notify/pipeline_failed_email.html.haml'
- - 'app/views/notify/pipeline_success_email.html.haml'
- 'app/views/notify/project_was_exported_email.html.haml'
- 'app/views/notify/project_was_moved_email.html.haml'
- 'app/views/notify/project_was_not_exported_email.html.haml'
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 1dcdb65d5c7..cc4b2dacab3 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -1,6 +1,7 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { n__, __, sprintf } from '~/locale';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
@@ -94,6 +95,9 @@ export default {
}
return version.versionIndex === -1;
},
+ isHead() {
+ return parseBoolean(getParameterByName('diff_head'));
+ },
isLatest(version) {
return (
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
@@ -121,7 +125,8 @@ export default {
<div>
<strong>
{{ versionName(version) }}
- <template v-if="isBase(version)">{{
+ <template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
+ <template v-else-if="isBase(version)">{{
s__('DiffsCompareBaseBranch|(base)')
}}</template>
</strong>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index ffcb0f24cc6..c901971be50 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -134,6 +134,10 @@ export default () => {
axios
.get(dataset.testReportsCountEndpoint)
.then(({ data }) => {
+ if (!data.total_count) {
+ return;
+ }
+
document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
})
.catch(() => {});
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index 72867ecd709..fa09e063552 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,5 +1,5 @@
<script>
-import _ from 'underscore';
+import { escape as esc } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
@@ -43,10 +43,10 @@ You are going to change the username %{currentUsernameBold} to %{newUsernameBold
Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
Please update your Git repository remotes as soon as possible.`),
{
- currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`,
- newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`,
- currentUsername: _.escape(this.username),
- newUsername: _.escape(this.newUsername),
+ currentUsernameBold: `<strong>${esc(this.username)}</strong>`,
+ newUsernameBold: `<strong>${esc(this.newUsername)}</strong>`,
+ currentUsername: esc(this.username),
+ newUsername: esc(this.newUsername),
},
false,
);
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 880e1a88975..55bc9fb8955 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import 'cropper';
-import _ from 'underscore';
+import { isString } from 'lodash';
(() => {
// Matches everything but the file name
@@ -29,7 +29,7 @@ import _ from 'underscore';
this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input);
- this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
+ this.modalCropImg = isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput
.attr('name', `${this.fileInput.attr('name')}-trigger`)
.attr('id', `${this.fileInput.attr('id')}-trigger`);
@@ -47,9 +47,9 @@ import _ from 'underscore';
this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl);
- this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
- this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
- this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
+ this.modalCrop = isString(modalCrop) ? $(modalCrop) : modalCrop;
+ this.uploadImageBtn = isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
+ this.modalCropImg = isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents();
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index e27ec571531..dabbcf0eac1 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -65,6 +65,7 @@
// Classes using mixins coming from @gitlab-ui
// can be removed once https://gitlab.com/gitlab-org/gitlab/merge_requests/19021 has been merged
+.gl-bg-blue-50 { @include gl-bg-blue-50; }
.gl-bg-red-100 { @include gl-bg-red-100; }
.gl-bg-orange-100 { @include gl-bg-orange-100; }
.gl-bg-gray-100 { @include gl-bg-gray-100; }
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 773b9fead3a..f2538d28a1a 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -10,6 +10,10 @@ module Emails
pipeline_mail(pipeline, recipients, 'failed')
end
+ def pipeline_fixed_email(pipeline, recipients)
+ pipeline_mail(pipeline, recipients, 'been fixed')
+ end
+
private
def pipeline_mail(pipeline, recipients, status)
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 381a4f54d9e..114737eb232 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -145,6 +145,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
+ def pipeline_fixed_email
+ Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email))
+ end
+
def autodevops_disabled_email
Notify.autodevops_disabled_email(pipeline, user.email).message
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 869a2e8da20..e07abc20dcf 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -63,6 +63,14 @@ module Ci
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id
has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline
+
+ has_one :ref_status, ->(pipeline) {
+ # We use .read_attribute to save 1 extra unneeded query to load the :project.
+ unscope(:where)
+ .where(project_id: pipeline.read_attribute(:project_id), ref: pipeline.ref, tag: pipeline.tag)
+ # Sadly :inverse_of is not supported (yet) by Rails for composite PKs.
+ }, class_name: 'Ci::Ref', inverse_of: :pipelines
+
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
@@ -227,7 +235,7 @@ module Ci
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
- PipelineNotificationWorker.perform_async(pipeline.id)
+ PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id)
end
end
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
new file mode 100644
index 00000000000..a0782bc0444
--- /dev/null
+++ b/app/models/ci/ref.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Ci
+ class Ref < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ STATUSES = %w[success failed fixed].freeze
+
+ belongs_to :project
+ belongs_to :last_updated_by_pipeline, foreign_key: :last_updated_by_pipeline_id, class_name: 'Ci::Pipeline'
+ # ActiveRecord doesn't support composite FKs for this reason we have to do the 'unscope(:where)'
+ # hack.
+ has_many :pipelines, ->(ref) {
+ # We use .read_attribute to save 1 extra unneeded query to load the :project.
+ unscope(:where)
+ .where(ref: ref.ref, project_id: ref.read_attribute(:project_id), tag: ref.tag)
+ # Sadly :inverse_of is not supported (yet) by Rails for composite PKs.
+ }, inverse_of: :ref_status
+
+ validates :status, inclusion: { in: STATUSES }
+ validates :last_updated_by_pipeline, presence: true
+ end
+end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 8e44e3d8e17..107d00d055a 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -52,7 +52,8 @@ class NotificationRecipient
when :mention
@type == :mention
when :participating
- @custom_action == :failed_pipeline || %i[participating mention].include?(@type)
+ %i[failed_pipeline fixed_pipeline].include?(@custom_action) ||
+ %i[participating mention].include?(@type)
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch
@@ -63,7 +64,13 @@ class NotificationRecipient
end
def custom_enabled?
- @custom_action && notification_setting&.event_enabled?(@custom_action)
+ return false unless @custom_action
+ return false unless notification_setting
+
+ notification_setting.event_enabled?(@custom_action) ||
+ # fixed_pipeline is a subset of success_pipeline event
+ (@custom_action == :fixed_pipeline &&
+ notification_setting.event_enabled?(:success_pipeline))
end
def unsubscribed?
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index e2c362538eb..38bd95e6a20 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -44,6 +44,7 @@ class NotificationSetting < ApplicationRecord
:reassign_merge_request,
:merge_merge_request,
:failed_pipeline,
+ :fixed_pipeline,
:success_pipeline
].freeze
@@ -76,9 +77,9 @@ class NotificationSetting < ApplicationRecord
setting
end
- # Allow people to receive failed pipeline notifications if they already have
- # custom notifications enabled, as these are more like mentions than the other
- # custom settings.
+ # Allow people to receive both failed pipeline/fixed pipeline notifications
+ # if they already have custom notifications enabled,
+ # as these are more like mentions than the other custom settings.
def failed_pipeline
bool = super
@@ -86,6 +87,13 @@ class NotificationSetting < ApplicationRecord
end
alias_method :failed_pipeline?, :failed_pipeline
+ def fixed_pipeline
+ bool = super
+
+ bool.nil? || bool
+ end
+ alias_method :fixed_pipeline?, :fixed_pipeline
+
def event_enabled?(event)
respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0f61d32eb8d..41c56fe6931 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -267,6 +267,7 @@ class Project < ApplicationRecord
class_name: 'Ci::Pipeline',
inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
+ has_many :ci_refs, class_name: 'Ci::Ref'
# Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 65bf8535d2a..c3ed958242b 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -49,7 +49,7 @@ class PipelinesEmailService < Service
return unless all_recipients.any?
pipeline_id = data[:object_attributes][:id]
- PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
+ PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients)
end
def can_test?
diff --git a/app/services/ci/update_ci_ref_status_service.rb b/app/services/ci/update_ci_ref_status_service.rb
new file mode 100644
index 00000000000..e5e5b94b629
--- /dev/null
+++ b/app/services/ci/update_ci_ref_status_service.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Ci
+ class UpdateCiRefStatusService
+ include Gitlab::OptimisticLocking
+
+ attr_reader :pipeline
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def call
+ save.tap { |success| after_save if success }
+ end
+
+ private
+
+ def save
+ might_insert = ref.new_record?
+
+ begin
+ retry_optimistic_lock(ref) do
+ next false if ref.persisted? &&
+ (ref.last_updated_by_pipeline_id || 0) >= pipeline.id
+
+ ref.update(status: next_status(ref.status, pipeline.status),
+ last_updated_by_pipeline: pipeline)
+ end
+ rescue ActiveRecord::RecordNotUnique
+ if might_insert
+ @ref = pipeline.reset.ref_status
+ might_insert = false
+ retry
+ else
+ raise
+ end
+ end
+ end
+
+ def next_status(ref_status, pipeline_status)
+ if ref_status == 'failed' && pipeline_status == 'success'
+ 'fixed'
+ else
+ pipeline_status
+ end
+ end
+
+ def after_save
+ enqueue_pipeline_notification
+ end
+
+ def enqueue_pipeline_notification
+ PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref.status)
+ end
+
+ def ref
+ @ref ||= pipeline.ref_status || build_ref
+ end
+
+ def build_ref
+ Ci::Ref.new(ref: pipeline.ref, project: pipeline.project, tag: pipeline.tag)
+ end
+ end
+end
diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb
index 8258efba6bf..f802aa44487 100644
--- a/app/services/merge_requests/create_pipeline_service.rb
+++ b/app/services/merge_requests/create_pipeline_service.rb
@@ -9,15 +9,10 @@ module MergeRequests
end
def create_detached_merge_request_pipeline(merge_request)
- if can_use_merge_request_ref?(merge_request)
- Ci::CreatePipelineService.new(merge_request.source_project, current_user,
- ref: merge_request.ref_path)
- .execute(:merge_request_event, merge_request: merge_request)
- else
- Ci::CreatePipelineService.new(merge_request.source_project, current_user,
- ref: merge_request.source_branch)
- .execute(:merge_request_event, merge_request: merge_request)
- end
+ Ci::CreatePipelineService.new(merge_request.source_project,
+ current_user,
+ ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request))
+ .execute(:merge_request_event, merge_request: merge_request)
end
def can_create_pipeline_for?(merge_request)
@@ -33,6 +28,16 @@ module MergeRequests
def allow_duplicate
params[:allow_duplicate]
end
+
+ private
+
+ def pipeline_ref_for_detached_merge_request_pipeline(merge_request)
+ if can_use_merge_request_ref?(merge_request)
+ merge_request.ref_path
+ else
+ merge_request.source_branch
+ end
+ end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ac7ef6fb970..6f2bfa8169b 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -434,18 +434,19 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
- def pipeline_finished(pipeline, recipients = nil)
+ def pipeline_finished(pipeline, ref_status: nil, recipients: nil)
# Must always check project configuration since recipients could be a list of emails
# from the PipelinesEmailService integration.
return if pipeline.project.emails_disabled?
- email_template = "pipeline_#{pipeline.status}_email"
+ ref_status ||= pipeline.status
+ email_template = "pipeline_#{ref_status}_email"
return unless mailer.respond_to?(email_template)
recipients ||= notifiable_users(
[pipeline.user], :watch,
- custom_action: :"#{pipeline.status}_pipeline",
+ custom_action: :"#{ref_status}_pipeline",
target: pipeline
).map do |user|
user.notification_email_for(pipeline.project.group)
diff --git a/app/views/notify/_successful_pipeline.html.haml b/app/views/notify/_successful_pipeline.html.haml
new file mode 100644
index 00000000000..231df2e9206
--- /dev/null
+++ b/app/views/notify/_successful_pipeline.html.haml
@@ -0,0 +1,118 @@
+- title = local_assigns[:title]
+%tr.table-success
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
+ %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
+ = title
+%tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+%tr.section
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
+ - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
+ - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
+ %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
+ = namespace_name
+ \/
+ %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
+ = @project.name
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
+ = @pipeline.source_ref
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = @pipeline.short_sha
+ - if @merge_request
+ in
+ %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
+ = @merge_request.to_reference
+ .commit{ style: "color:#5c5c5c;font-weight:300;" }
+ = @pipeline.git_commit_message.truncate(50)
+ - commit = @pipeline.commit
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon_for(commit.author, commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ - if commit.author
+ %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
+ = commit.author.name
+ - else
+ %span
+ = commit.author_name
+ - if commit.different_committer?
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon_for(commit.committer, commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ - if commit.committer
+ %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
+ = commit.committer.name
+ - else
+ %span
+ = commit.committer_name
+
+%tr.spacer
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
+ &nbsp;
+%tr.success-message
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ triggered by
+ - if @pipeline.user
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
+ %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
+ %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
+ = @pipeline.user.name
+ - else
+ %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
+ API
+%tr
+ %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
+ - job_count = @pipeline.total_size
+ - stage_count = @pipeline.stages_count
+ successfully completed
+ #{job_count} #{'job'.pluralize(job_count)}
+ in
+ #{stage_count} #{'stage'.pluralize(stage_count)}.
diff --git a/app/views/notify/_successful_pipeline.text.erb b/app/views/notify/_successful_pipeline.text.erb
new file mode 100644
index 00000000000..628976e2dda
--- /dev/null
+++ b/app/views/notify/_successful_pipeline.text.erb
@@ -0,0 +1,32 @@
+<%= local_assigns[:title] %>
+
+Project: <%= @project.name %> ( <%= project_url(@project) %> )
+Branch: <%= @pipeline.source_ref %> ( <%= commits_url(@pipeline) %> )
+<% if @merge_request -%>
+Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
+<% end -%>
+
+Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
+Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
+<% commit = @pipeline.commit -%>
+<% if commit.author -%>
+Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
+<% else -%>
+Commit Author: <%= commit.author_name %>
+<% end -%>
+<% if commit.different_committer? -%>
+<% if commit.committer -%>
+Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
+<% else -%>
+Committed by: <%= commit.committer_name %>
+<% end -%>
+<% end -%>
+
+<% job_count = @pipeline.total_size -%>
+<% stage_count = @pipeline.stages_count -%>
+<% if @pipeline.user -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
+<% else -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
+<% end -%>
+successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
diff --git a/app/views/notify/pipeline_fixed_email.html.haml b/app/views/notify/pipeline_fixed_email.html.haml
new file mode 100644
index 00000000000..05c0027a6fc
--- /dev/null
+++ b/app/views/notify/pipeline_fixed_email.html.haml
@@ -0,0 +1 @@
+= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!'
diff --git a/app/views/notify/pipeline_fixed_email.text.erb b/app/views/notify/pipeline_fixed_email.text.erb
new file mode 100644
index 00000000000..75268531bdc
--- /dev/null
+++ b/app/views/notify/pipeline_fixed_email.text.erb
@@ -0,0 +1 @@
+<%= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!' -%>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index e575a5569fa..c34e02b5fee 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,117 +1 @@
-%tr.table-success
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
- %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
- Your pipeline has passed.
-%tr.spacer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
- &nbsp;
-%tr.section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
- - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
- %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
- = namespace_name
- \/
- %a.muted{ href: project_url(@project), style: "color:#333333;text-decoration:none;" }
- = @project.name
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
- = @pipeline.source_ref
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = @pipeline.short_sha
- - if @merge_request
- in
- %a{ href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;" }
- = @merge_request.to_reference
- .commit{ style: "color:#5c5c5c;font-weight:300;" }
- = @pipeline.git_commit_message.truncate(50)
- - commit = @pipeline.commit
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon_for(commit.author, commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- - if commit.author
- %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
- = commit.author.name
- - else
- %span
- = commit.author_name
- - if commit.different_committer?
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon_for(commit.committer, commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- - if commit.committer
- %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
- = commit.committer.name
- - else
- %span
- = commit.committer_name
-
-%tr.spacer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
- &nbsp;
-%tr.success-message
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
- triggered by
- - if @pipeline.user
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
- %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
- %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
- = @pipeline.user.name
- - else
- %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
- API
-%tr
- %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
- - job_count = @pipeline.total_size
- - stage_count = @pipeline.stages_count
- successfully completed
- #{job_count} #{'job'.pluralize(job_count)}
- in
- #{stage_count} #{'stage'.pluralize(stage_count)}.
+= render 'notify/successful_pipeline', title: 'Your pipeline has passed.'
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 4005158dc9e..b554bffc908 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -1,32 +1 @@
-Your pipeline has passed.
-
-Project: <%= @project.name %> ( <%= project_url(@project) %> )
-Branch: <%= @pipeline.source_ref %> ( <%= commits_url(@pipeline) %> )
-<% if @merge_request -%>
-Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
-<% end -%>
-
-Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
-Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
-<% commit = @pipeline.commit -%>
-<% if commit.author -%>
-Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
-<% else -%>
-Commit Author: <%= commit.author_name %>
-<% end -%>
-<% if commit.different_committer? -%>
-<% if commit.committer -%>
-Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
-<% else -%>
-Committed by: <%= commit.committer_name %>
-<% end -%>
-<% end -%>
-
-<% job_count = @pipeline.total_size -%>
-<% stage_count = @pipeline.stages_count -%>
-<% if @pipeline.user -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
-<% else -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
-<% end -%>
-successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+<%= render 'notify/successful_pipeline', title: 'Your pipeline has passed.' -%>
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e8682769720..0b7add65d94 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -668,6 +668,13 @@
:resource_boundary: :cpu
:weight: 3
:idempotent:
+- :name: pipeline_default:pipeline_update_ci_ref_status
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :latency_sensitive: true
+ :resource_boundary: :cpu
+ :weight: 3
+ :idempotent:
- :name: pipeline_hooks:build_hooks
:feature_category: :continuous_integration
:has_external_dependencies:
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index e9081cc416f..72663fa19ae 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -8,12 +8,20 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
- def perform(pipeline_id, recipients = nil)
- pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+ def perform(pipeline_id, args = {})
+ case args
+ when Hash
+ ref_status = args[:ref_status]
+ recipients = args[:recipients]
+ else # TODO: backward compatible interface, can be removed in 12.10
+ recipients = args
+ ref_status = nil
+ end
+ pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
- NotificationService.new.pipeline_finished(pipeline, recipients)
+ NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_update_ci_ref_status_worker.rb b/app/workers/pipeline_update_ci_ref_status_worker.rb
new file mode 100644
index 00000000000..3d6a0d30e9c
--- /dev/null
+++ b/app/workers/pipeline_update_ci_ref_status_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class PipelineUpdateCiRefStatusWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
+
+ def perform(pipeline_id)
+ pipeline = Ci::Pipeline.find_by_id(pipeline_id)
+
+ return unless pipeline
+
+ Ci::UpdateCiRefStatusService.new(pipeline).call
+ end
+end
diff --git a/changelogs/unreleased/11821-insights-back-json-fix.yml b/changelogs/unreleased/11821-insights-back-json-fix.yml
new file mode 100644
index 00000000000..69fde018420
--- /dev/null
+++ b/changelogs/unreleased/11821-insights-back-json-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Insights displaying JSON on back navigation
+merge_request: 25801
+author:
+type: fixed
diff --git a/changelogs/unreleased/207455-frontend-fix-epic-blform.yml b/changelogs/unreleased/207455-frontend-fix-epic-blform.yml
new file mode 100644
index 00000000000..3b4ff6dda36
--- /dev/null
+++ b/changelogs/unreleased/207455-frontend-fix-epic-blform.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "Add an epic" form
+merge_request: 26003
+author:
+type: fixed
diff --git a/changelogs/unreleased/24309-notifications-for-when-pipelines-are-fixed.yml b/changelogs/unreleased/24309-notifications-for-when-pipelines-are-fixed.yml
new file mode 100644
index 00000000000..fc2d1ecbef1
--- /dev/null
+++ b/changelogs/unreleased/24309-notifications-for-when-pipelines-are-fixed.yml
@@ -0,0 +1,5 @@
+---
+title: Notifications for when pipelines are fixed
+merge_request: 16951
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/jdb-display-base-label-versions-dropdown.yml b/changelogs/unreleased/jdb-display-base-label-versions-dropdown.yml
new file mode 100644
index 00000000000..e18f2f1857b
--- /dev/null
+++ b/changelogs/unreleased/jdb-display-base-label-versions-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Display base label in versions drop down
+merge_request: 25834
+author:
+type: added
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 5f07b486aea..156cf78dfc4 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -245,6 +245,12 @@ Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
#
+# Service desk email
+#
+Settings['service_desk_email'] ||= Settingslogic.new({})
+Settings.service_desk_email['enabled'] = false if Settings.service_desk_email['enabled'].nil?
+
+#
# Build Artifacts
#
Settings['artifacts'] ||= Settingslogic.new({})
diff --git a/db/migrate/20191001170300_create_ci_ref.rb b/db/migrate/20191001170300_create_ci_ref.rb
new file mode 100644
index 00000000000..af25e67430b
--- /dev/null
+++ b/db/migrate/20191001170300_create_ci_ref.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateCiRef < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :ci_refs do |t|
+ t.references :project, null: false, index: false, foreign_key: { on_delete: :cascade }, type: :integer
+ t.integer :lock_version, default: 0
+ t.integer :last_updated_by_pipeline_id
+ t.boolean :tag, default: false, null: false
+ t.string :ref, null: false, limit: 255
+ t.string :status, null: false, limit: 255
+ t.foreign_key :ci_pipelines, column: :last_updated_by_pipeline_id, on_delete: :nullify
+ t.index [:project_id, :ref, :tag], unique: true
+ t.index [:last_updated_by_pipeline_id]
+ end
+ end
+end
diff --git a/db/migrate/20191111165017_add_fixed_pipeline_to_notification_settings.rb b/db/migrate/20191111165017_add_fixed_pipeline_to_notification_settings.rb
new file mode 100644
index 00000000000..7a857807468
--- /dev/null
+++ b/db/migrate/20191111165017_add_fixed_pipeline_to_notification_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddFixedPipelineToNotificationSettings < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :notification_settings, :fixed_pipeline, :boolean
+ end
+end
diff --git a/db/migrate/20200226162156_rename_closed_at_to_dismissed_at_in_vulnerabilities.rb b/db/migrate/20200226162156_rename_closed_at_to_dismissed_at_in_vulnerabilities.rb
new file mode 100644
index 00000000000..ce7170cb335
--- /dev/null
+++ b/db/migrate/20200226162156_rename_closed_at_to_dismissed_at_in_vulnerabilities.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RenameClosedAtToDismissedAtInVulnerabilities < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :vulnerabilities, :closed_at, :dismissed_at
+ end
+
+ def down
+ undo_rename_column_concurrently :vulnerabilities, :closed_at, :dismissed_at
+ end
+end
diff --git a/db/migrate/20200226162634_rename_closed_by_to_dismissed_by_in_vulnerabilities.rb b/db/migrate/20200226162634_rename_closed_by_to_dismissed_by_in_vulnerabilities.rb
new file mode 100644
index 00000000000..04b1f0d7136
--- /dev/null
+++ b/db/migrate/20200226162634_rename_closed_by_to_dismissed_by_in_vulnerabilities.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RenameClosedByToDismissedByInVulnerabilities < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :vulnerabilities, :closed_by_id, :dismissed_by_id
+ end
+
+ def down
+ undo_rename_column_concurrently :vulnerabilities, :closed_by_id, :dismissed_by_id
+ end
+end
diff --git a/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb b/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb
index 97f2e568a7e..2cbf7a69159 100644
--- a/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb
+++ b/db/post_migrate/20191115115043_migrate_epic_mentions_to_db.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
DOWNTIME = false
disable_ddl_transaction!
@@ -26,7 +28,7 @@ class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
.where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first
- BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
+ migrate_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
end
end
diff --git a/db/post_migrate/20200214173000_cleanup_empty_epic_user_mentions.rb b/db/post_migrate/20200214173000_cleanup_empty_epic_user_mentions.rb
new file mode 100644
index 00000000000..ef6486675e0
--- /dev/null
+++ b/db/post_migrate/20200214173000_cleanup_empty_epic_user_mentions.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class CleanupEmptyEpicUserMentions < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+ BATCH_SIZE = 10000
+
+ class EpicUserMention < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'epic_user_mentions'
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ # cleanup epic user mentions with no actual mentions,
+ # re https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24586#note_285982468
+ EpicUserMention
+ .where(mentioned_users_ids: nil)
+ .where(mentioned_groups_ids: nil)
+ .where(mentioned_projects_ids: nil)
+ .each_batch(of: BATCH_SIZE) do |batch|
+ batch.delete_all
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20200214174519_remigrate_epic_mentions_to_db.rb b/db/post_migrate/20200214174519_remigrate_epic_mentions_to_db.rb
new file mode 100644
index 00000000000..68fe031be5d
--- /dev/null
+++ b/db/post_migrate/20200214174519_remigrate_epic_mentions_to_db.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class RemigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ DELAY = 2.minutes.to_i
+ BATCH_SIZE = 10000
+ MIGRATION = 'UserMentions::CreateResourceUserMention'
+
+ JOIN = "LEFT JOIN epic_user_mentions on epics.id = epic_user_mentions.epic_id"
+ QUERY_CONDITIONS = "(description like '%@%' OR title like '%@%') AND epic_user_mentions.epic_id is null"
+
+ class Epic < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'epics'
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ Epic
+ .joins(JOIN)
+ .where(QUERY_CONDITIONS)
+ .each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first
+ migrate_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20200214174607_remigrate_epic_notes_mentions_to_db.rb b/db/post_migrate/20200214174607_remigrate_epic_notes_mentions_to_db.rb
new file mode 100644
index 00000000000..cb442233229
--- /dev/null
+++ b/db/post_migrate/20200214174607_remigrate_epic_notes_mentions_to_db.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class RemigrateEpicNotesMentionsToDb < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ DELAY = 2.minutes.to_i
+ BATCH_SIZE = 10000
+ MIGRATION = 'UserMentions::CreateResourceUserMention'
+
+ INDEX_NAME = 'epic_mentions_temp_index'
+ INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Epic'"
+ QUERY_CONDITIONS = "#{INDEX_CONDITION} AND epic_user_mentions.epic_id IS NULL"
+ JOIN = 'INNER JOIN epics ON epics.id = notes.noteable_id LEFT JOIN epic_user_mentions ON notes.id = epic_user_mentions.note_id'
+
+ class Note < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'notes'
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ # create temporary index for notes with mentions, may take well over 1h
+ add_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME)
+
+ Note
+ .joins(JOIN)
+ .where(QUERY_CONDITIONS)
+ .each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck(Arel.sql('MIN(notes.id)'), Arel.sql('MAX(notes.id)')).first
+ migrate_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, true, *range])
+ end
+ end
+
+ def down
+ # no-op
+ # temporary index is to be dropped in a different migration in an upcoming release:
+ # https://gitlab.com/gitlab-org/gitlab/issues/196842
+ end
+end
diff --git a/db/post_migrate/20200226162239_cleanup_closed_at_rename_in_vulnerabilities.rb b/db/post_migrate/20200226162239_cleanup_closed_at_rename_in_vulnerabilities.rb
new file mode 100644
index 00000000000..eb7df0b8d22
--- /dev/null
+++ b/db/post_migrate/20200226162239_cleanup_closed_at_rename_in_vulnerabilities.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CleanupClosedAtRenameInVulnerabilities < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :vulnerabilities, :closed_at, :dismissed_at
+ end
+
+ def down
+ undo_cleanup_concurrent_column_rename :vulnerabilities, :closed_at, :dismissed_at
+ end
+end
diff --git a/db/post_migrate/20200226162723_cleanup_closed_by_rename_in_vulnerabilities.rb b/db/post_migrate/20200226162723_cleanup_closed_by_rename_in_vulnerabilities.rb
new file mode 100644
index 00000000000..4aa3568db14
--- /dev/null
+++ b/db/post_migrate/20200226162723_cleanup_closed_by_rename_in_vulnerabilities.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CleanupClosedByRenameInVulnerabilities < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :vulnerabilities, :closed_by_id, :dismissed_by_id
+ end
+
+ def down
+ undo_cleanup_concurrent_column_rename :vulnerabilities, :closed_by_id, :dismissed_by_id
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f49b1af4ea9..623fdcd0542 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_02_24_163804) do
+ActiveRecord::Schema.define(version: 2020_02_26_162723) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -878,6 +878,17 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.index ["pipeline_id"], name: "index_ci_pipelines_config_on_pipeline_id"
end
+ create_table "ci_refs", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "lock_version", default: 0
+ t.integer "last_updated_by_pipeline_id"
+ t.boolean "tag", default: false, null: false
+ t.string "ref", limit: 255, null: false
+ t.string "status", limit: 255, null: false
+ t.index ["last_updated_by_pipeline_id"], name: "index_ci_refs_on_last_updated_by_pipeline_id"
+ t.index ["project_id", "ref", "tag"], name: "index_ci_refs_on_project_id_and_ref_and_tag", unique: true
+ end
+
create_table "ci_resource_groups", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -2845,6 +2856,7 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.boolean "issue_due"
t.boolean "new_epic"
t.string "notification_email"
+ t.boolean "fixed_pipeline"
t.boolean "new_release"
t.index ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type"
t.index ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true
@@ -4356,8 +4368,6 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.text "description_html"
t.bigint "start_date_sourcing_milestone_id"
t.bigint "due_date_sourcing_milestone_id"
- t.bigint "closed_by_id"
- t.datetime_with_timezone "closed_at"
t.integer "state", limit: 2, default: 1, null: false
t.integer "severity", limit: 2, null: false
t.boolean "severity_overridden", default: false
@@ -4369,9 +4379,11 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.integer "cached_markdown_version"
t.bigint "confirmed_by_id"
t.datetime_with_timezone "confirmed_at"
+ t.datetime_with_timezone "dismissed_at"
+ t.bigint "dismissed_by_id"
t.index ["author_id"], name: "index_vulnerabilities_on_author_id"
- t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id"
t.index ["confirmed_by_id"], name: "index_vulnerabilities_on_confirmed_by_id"
+ t.index ["dismissed_by_id"], name: "index_vulnerabilities_on_dismissed_by_id"
t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id"
t.index ["epic_id"], name: "index_vulnerabilities_on_epic_id"
t.index ["last_edited_by_id"], name: "index_vulnerabilities_on_last_edited_by_id"
@@ -4650,6 +4662,8 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_pipelines_config", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
+ add_foreign_key "ci_refs", "ci_pipelines", column: "last_updated_by_pipeline_id", on_delete: :nullify
+ add_foreign_key "ci_refs", "projects", on_delete: :cascade
add_foreign_key "ci_resource_groups", "projects", name: "fk_774722d144", on_delete: :cascade
add_foreign_key "ci_resources", "ci_builds", column: "build_id", name: "fk_e169a8e3d5", on_delete: :nullify
add_foreign_key "ci_resources", "ci_resource_groups", column: "resource_group_id", on_delete: :cascade
@@ -5026,8 +5040,8 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
add_foreign_key "vulnerabilities", "milestones", name: "fk_131d289c65", on_delete: :nullify
add_foreign_key "vulnerabilities", "projects", name: "fk_efb96ab1e2", on_delete: :cascade
add_foreign_key "vulnerabilities", "users", column: "author_id", name: "fk_b1de915a15", on_delete: :nullify
- add_foreign_key "vulnerabilities", "users", column: "closed_by_id", name: "fk_cf5c60acbf", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "confirmed_by_id", name: "fk_959d40ad0a", on_delete: :nullify
+ add_foreign_key "vulnerabilities", "users", column: "dismissed_by_id", name: "fk_725465b774", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "last_edited_by_id", name: "fk_1302949740", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "resolved_by_id", name: "fk_76bc5f5455", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "updated_by_id", name: "fk_7ac31eacb9", on_delete: :nullify
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 5cf48ab901d..6b62b11750c 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -1863,6 +1863,12 @@ type Epic implements Noteable {
descendantCounts: EpicDescendantCount
"""
+ Total weight of open and closed descendant epic's issues. Available only when
+ feature flag unfiltered_epic_aggregates is enabled.
+ """
+ descendantWeightSum: EpicDescendantWeights
+
+ """
Description of the epic
"""
description: String
@@ -2179,6 +2185,21 @@ type EpicDescendantCount {
}
"""
+Total weight of open and closed descendant issues
+"""
+type EpicDescendantWeights {
+ """
+ Total weight of completed (closed) issues in this epic, including epic descendants
+ """
+ closedIssues: Int
+
+ """
+ Total weight of opened issues in this epic, including epic descendants
+ """
+ openedIssues: Int
+}
+
+"""
An edge in a connection.
"""
type EpicEdge {
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 978fbc35125..1a9be3f496b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -13,6 +13,9 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
Each table below documents a GraphQL type. Types match loosely to models, but not all
fields and methods on a model are available via GraphQL.
+CAUTION: **Caution:**
+Fields that are deprecated are marked with **{warning-solid}**.
+
## AddAwardEmojiPayload
Autogenerated return type of AddAwardEmoji
@@ -69,7 +72,7 @@ Represents a project or group board
| `authoredDate` | Time | Timestamp of when the commit was authored |
| `description` | String | Description of the commit message |
| `id` | ID! | ID (global ID) of the commit |
-| `latestPipeline` | Pipeline | Latest pipeline of the commit |
+| `latestPipeline` **{warning-solid}** | Pipeline | **Deprecated:** Use pipelines |
| `message` | String | Raw commit message |
| `sha` | String! | SHA1 ID of the commit |
| `signatureHtml` | String | Rendered HTML of the commit signature |
@@ -294,6 +297,7 @@ Represents an epic.
| `closedAt` | Time | Timestamp of the epic's closure |
| `createdAt` | Time | Timestamp of the epic's creation |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
+| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed descendant epic's issues. Available only when feature flag unfiltered_epic_aggregates is enabled. |
| `description` | String | Description of the epic |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
@@ -334,6 +338,15 @@ Counts of descendent epics.
| `openedEpics` | Int | Number of opened sub-epics |
| `openedIssues` | Int | Number of opened epic issues |
+## EpicDescendantWeights
+
+Total weight of open and closed descendant issues
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `closedIssues` | Int | Total weight of completed (closed) issues in this epic, including epic descendants |
+| `openedIssues` | Int | Total weight of opened issues in this epic, including epic descendants |
+
## EpicIssue
Relationship between an epic and an issue
@@ -347,7 +360,7 @@ Relationship between an epic and an issue
| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue |
-| `designs` | DesignCollection | Deprecated. Use `designCollection` |
+| `designs` **{warning-solid}** | DesignCollection | **Deprecated:** Use designCollection |
| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
@@ -417,7 +430,7 @@ Autogenerated return type of EpicTreeReorder
| `enabled` | Boolean! | Indicates whether Grafana integration is enabled |
| `grafanaUrl` | String! | Url for the Grafana host for the Grafana integration |
| `id` | ID! | Internal ID of the Grafana integration |
-| `token` | String! | API token for the Grafana integration. Field is permanently masked. |
+| `token` **{warning-solid}** | String! | **Deprecated:** Plain text token has been masked for security reasons |
| `updatedAt` | Time! | Timestamp of the issue's last activity |
## Group
@@ -469,7 +482,7 @@ Autogenerated return type of EpicTreeReorder
| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue |
-| `designs` | DesignCollection | Deprecated. Use `designCollection` |
+| `designs` **{warning-solid}** | DesignCollection | **Deprecated:** Use designCollection |
| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
@@ -578,7 +591,7 @@ Autogenerated return type of MarkAsSpamSnippet
| `id` | ID! | ID of the merge request |
| `iid` | String! | Internal ID of the merge request |
| `inProgressMergeCommitSha` | String | Commit SHA of the merge request if merge is in progress |
-| `mergeCommitMessage` | String | Deprecated - renamed to defaultMergeCommitMessage |
+| `mergeCommitMessage` **{warning-solid}** | String | **Deprecated:** Renamed to defaultMergeCommitMessage |
| `mergeCommitSha` | String | SHA of the merge request commit (set once merged) |
| `mergeError` | String | Error message due to a merge error |
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index 38a5e350bee..596365743fa 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -30,6 +30,7 @@ If the `custom` level is used, specific email events can be controlled. Availabl
- `reassign_merge_request`
- `merge_merge_request`
- `failed_pipeline`
+- `fixed_pipeline`
- `success_pipeline`
- `new_epic` **(ULTIMATE)**
@@ -83,6 +84,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `fixed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6626) in 11.3) **(ULTIMATE)** |
@@ -152,6 +154,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `fixed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6626) in 11.3) **(ULTIMATE)** |
@@ -178,6 +181,7 @@ Example responses:
"reassign_merge_request": false,
"merge_merge_request": false,
"failed_pipeline": false,
+ "fixed_pipeline": false,
"success_pipeline": false
}
}
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index cffd261e757..9ec56d304e0 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -50,12 +50,12 @@ For indexing Git repository data, GitLab uses an [indexer written in Go](https:/
The way you install the Go indexer depends on your version of GitLab:
- For GitLab Omnibus 11.8 and above, see [GitLab Omnibus](#gitlab-omnibus).
-- For older versions of GitLab, install the indexer [From Source](#from-source).
+- For installations from source or older versions of GitLab Omnibus, install the indexer [From Source](#from-source).
### GitLab Omnibus
-The Go indexer was included in Omnibus GitLab 11.8 as an optional replacement to a
-Ruby-based indexer. [Since GitLab v12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481),
+Since GitLab 11.8 the Go indexer is included in GitLab Omnibus.
+The former Ruby-based indexer was removed in [GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481).
### From source
diff --git a/doc/user/application_security/license_compliance/index.md b/doc/user/application_security/license_compliance/index.md
index dfe7a714f6c..03a7ffd195a 100644
--- a/doc/user/application_security/license_compliance/index.md
+++ b/doc/user/application_security/license_compliance/index.md
@@ -216,7 +216,7 @@ If you're using a custom setup for License Compliance, you're required
to update your CI config accordingly:
1. Change the CI template to `License-Scanning.gitlab-ci.yml`.
-1. Change the job name to `license_management` (if you mention it in `.gitlab-ci.yml`).
+1. Change the job name to `license_scanning` (if you mention it in `.gitlab-ci.yml`).
1. Change the artifact name to `gl-license-scanning-report.json` (if you mention it in `.gitlab-ci.yml`).
For example, the following `.gitlab-ci.yml`:
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 96ae8d04e03..588c471bb64 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -178,7 +178,8 @@ In most of the below cases, the notification will be sent to:
| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The author of the pipeline |
-| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
+| Fixed pipeline | The author of the pipeline |
+| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set. If the pipeline failed previously, a `Fixed pipeline` message will be sent for the first successful pipeline after the failure, then a `Successful pipeline` message for any further successful pipelines. |
| New epic **(ULTIMATE)** | |
| Close epic **(ULTIMATE)** | |
| Reopen epic **(ULTIMATE)** | |
diff --git a/lib/gitlab/background_migration/user_mentions/models/epic.rb b/lib/gitlab/background_migration/user_mentions/models/epic.rb
index 9797c86478e..ad1d904cde7 100644
--- a/lib/gitlab/background_migration/user_mentions/models/epic.rb
+++ b/lib/gitlab/background_migration/user_mentions/models/epic.rb
@@ -18,7 +18,6 @@ module Gitlab
self.table_name = 'epics'
belongs_to :author, class_name: "User"
- belongs_to :project
belongs_to :group
def self.user_mention_model
diff --git a/lib/gitlab/checks/snippet_check.rb b/lib/gitlab/checks/snippet_check.rb
index 26dd772764a..be25fe3e7c4 100644
--- a/lib/gitlab/checks/snippet_check.rb
+++ b/lib/gitlab/checks/snippet_check.rb
@@ -21,7 +21,7 @@ module Gitlab
def exec
if creation? || deletion?
- raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_delete_branch]
+ raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch]
end
# TODO: https://gitlab.com/gitlab-org/gitlab/issues/205628
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index f028102da9b..b7b9288517d 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -34,8 +34,7 @@ module Gitlab
ignore_auto_reply!(mail)
- mail_key = extract_mail_key(mail)
- handler = Handler.for(mail, mail_key)
+ handler = find_handler(mail)
raise UnknownIncomingEmail unless handler
@@ -46,6 +45,11 @@ module Gitlab
private
+ def find_handler(mail)
+ mail_key = extract_mail_key(mail)
+ Handler.for(mail, mail_key)
+ end
+
def build_mail
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index ff1af9bede4..3956fc8a483 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -28,7 +28,7 @@ module Gitlab
# TODO: Investigate if expanding actor/authentication types are needed.
# https://gitlab.com/gitlab-org/gitlab/issues/202190
if actor && !actor.is_a?(User) && !actor.instance_of?(Key)
- raise UnauthorizedError, ERROR_MESSAGES[:authentication_mechanism]
+ raise ForbiddenError, ERROR_MESSAGES[:authentication_mechanism]
end
unless Feature.enabled?(:version_snippets, user)
@@ -53,7 +53,7 @@ module Gitlab
override :check_push_access!
def check_push_access!
- raise UnauthorizedError, ERROR_MESSAGES[:update_snippet] unless user
+ raise ForbiddenError, ERROR_MESSAGES[:update_snippet] unless user
check_change_access!
end
@@ -74,7 +74,7 @@ module Gitlab
passed = guest_can_download_code? || user_can_download_code?
unless passed
- raise UnauthorizedError, ERROR_MESSAGES[:read_snippet]
+ raise ForbiddenError, ERROR_MESSAGES[:read_snippet]
end
end
@@ -91,7 +91,7 @@ module Gitlab
override :check_change_access!
def check_change_access!
unless user_access.can_do_action?(:update_snippet)
- raise UnauthorizedError, ERROR_MESSAGES[:update_snippet]
+ raise ForbiddenError, ERROR_MESSAGES[:update_snippet]
end
changes_list.each do |change|
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb
index 56524120ffd..0dd28b32511 100644
--- a/lib/gitlab/graphql/docs/helper.rb
+++ b/lib/gitlab/graphql/docs/helper.rb
@@ -25,6 +25,28 @@ module Gitlab
fields.sort_by { |field| field[:name] }
end
+ def render_field(field)
+ '| %s | %s | %s |' % [
+ render_field_name(field),
+ render_field_type(field[:type][:info]),
+ render_field_description(field)
+ ]
+ end
+
+ def render_field_name(field)
+ rendered_name = "`#{field[:name]}`"
+ rendered_name += ' **{warning-solid}**' if field[:is_deprecated]
+ rendered_name
+ end
+
+ # Returns the field description. If the field has been deprecated,
+ # the deprecation reason will be returned in place of the description.
+ def render_field_description(field)
+ return field[:description] unless field[:is_deprecated]
+
+ "**Deprecated:** #{field[:deprecation_reason]}"
+ end
+
# Some fields types are arrays of other types and are displayed
# on docs wrapped in square brackets, for example: [String!].
# This makes GitLab docs renderer thinks they are links so here
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index b126a22c301..8c033526557 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -11,6 +11,9 @@
Each table below documents a GraphQL type. Types match loosely to models, but not all
fields and methods on a model are available via GraphQL.
+
+ CAUTION: **Caution:**
+ Fields that are deprecated are marked with **{warning-solid}**.
\
- objects.each do |type|
- unless type[:fields].empty?
@@ -22,5 +25,5 @@
~ "| Name | Type | Description |"
~ "| --- | ---- | ---------- |"
- sorted_fields(type[:fields]).each do |field|
- = "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
+ = render_field(field)
\
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 4547a9b0a01..2889dbc68cc 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -28,8 +28,9 @@ module Gitlab
config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end
- def key_from_address(address)
- regex = address_regex
+ def key_from_address(address, wildcard_address: nil)
+ wildcard_address ||= config.address
+ regex = address_regex(wildcard_address)
return unless regex
match = address.match(regex)
@@ -55,8 +56,7 @@ module Gitlab
private
- def address_regex
- wildcard_address = config.address
+ def address_regex(wildcard_address)
return unless wildcard_address
regex = Regexp.escape(wildcard_address)
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index 6c27213df49..c3a52a1986d 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -20,6 +20,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
chain.add Gitlab::SidekiqStatus::ServerMiddleware
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
+ chain.add Gitlab::SidekiqMiddleware::DuplicateJobs::Server
end
end
@@ -33,6 +34,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add Labkit::Middleware::Sidekiq::Client
chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
+ chain.add Gitlab::SidekiqMiddleware::DuplicateJobs::Client
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
new file mode 100644
index 00000000000..bb0c18735bb
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/client.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module DuplicateJobs
+ class Client
+ def call(worker_class, job, queue, _redis_pool, &block)
+ DuplicateJob.new(job, queue).schedule(&block)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
new file mode 100644
index 00000000000..b84673c4cee
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'digest'
+
+module Gitlab
+ module SidekiqMiddleware
+ module DuplicateJobs
+ # This class defines an identifier of a job in a queue
+ # The identifier based on a job's class and arguments.
+ #
+ # As strategy decides when to keep track of the job in redis and when to
+ # remove it.
+ #
+ # Storing the deduplication key in redis can be done by calling `check!`
+ # check returns the `jid` of the job if it was scheduled, or the `jid` of
+ # the duplicate job if it was already scheduled
+ #
+ # When new jobs can be scheduled again, the strategy calls `#delete`.
+ class DuplicateJob
+ DUPLICATE_KEY_TTL = 6.hours
+
+ attr_reader :existing_jid
+
+ def initialize(job, queue_name, strategy: :until_executing)
+ @job = job
+ @queue_name = queue_name
+ @strategy = strategy
+ end
+
+ # This will continue the middleware chain if the job should be scheduled
+ # It will return false if the job needs to be cancelled
+ def schedule(&block)
+ Strategies.for(strategy).new(self).schedule(job, &block)
+ end
+
+ # This will continue the server middleware chain if the job should be
+ # executed.
+ # It will return false if the job should not be executed.
+ def perform(&block)
+ Strategies.for(strategy).new(self).perform(job, &block)
+ end
+
+ # This method will return the jid that was set in redis
+ def check!
+ read_jid = nil
+
+ Sidekiq.redis do |redis|
+ redis.multi do |multi|
+ redis.set(idempotency_key, jid, ex: DUPLICATE_KEY_TTL, nx: true)
+ read_jid = redis.get(idempotency_key)
+ end
+ end
+
+ self.existing_jid = read_jid.value
+ end
+
+ def delete!
+ Sidekiq.redis do |redis|
+ redis.del(idempotency_key)
+ end
+ end
+
+ def duplicate?
+ raise "Call `#check!` first to check for existing duplicates" unless existing_jid
+
+ jid != existing_jid
+ end
+
+ private
+
+ attr_reader :queue_name, :strategy, :job
+ attr_writer :existing_jid
+
+ def worker_class_name
+ job['class']
+ end
+
+ def arguments
+ job['args']
+ end
+
+ def jid
+ job['jid']
+ end
+
+ def idempotency_key
+ @idempotency_key ||= "#{namespace}:#{idempotency_hash}"
+ end
+
+ def idempotency_hash
+ Digest::SHA256.hexdigest(idempotency_string)
+ end
+
+ def namespace
+ "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:duplicate:#{queue_name}"
+ end
+
+ def idempotency_string
+ "#{worker_class_name}:#{arguments.join('-')}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/server.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/server.rb
new file mode 100644
index 00000000000..a35edc5774e
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/server.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module DuplicateJobs
+ class Server
+ def call(worker, job, queue, &block)
+ DuplicateJob.new(job, queue).perform(&block)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb
new file mode 100644
index 00000000000..a08310a58ff
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module DuplicateJobs
+ module Strategies
+ UnknownStrategyError = Class.new(StandardError)
+
+ STRATEGIES = {
+ until_executing: UntilExecuting
+ }.freeze
+
+ def self.for(name)
+ STRATEGIES.fetch(name)
+ rescue KeyError
+ raise UnknownStrategyError, "Unknown deduplication strategy #{name}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing.rb
new file mode 100644
index 00000000000..b8f49b67a59
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ module DuplicateJobs
+ module Strategies
+ # This strategy takes a lock before scheduling the job in a queue and
+ # removes the lock before the job starts allowing a new job to be queued
+ # while a job is still executing.
+ class UntilExecuting
+ def initialize(duplicate_job)
+ @duplicate_job = duplicate_job
+ end
+
+ def schedule(job)
+ if duplicate_job.check! && duplicate_job.duplicate?
+ job['duplicate-of'] = duplicate_job.existing_jid
+ end
+
+ yield
+ end
+
+ def perform(_job)
+ duplicate_job.delete!
+
+ yield
+ end
+
+ private
+
+ attr_reader :duplicate_job
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a425217fbe9..ad20e87eb68 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -196,6 +196,11 @@ msgid_plural "%d unstaged changes"
msgstr[0] ""
msgstr[1] ""
+msgid "%d vulnerability dismissed"
+msgid_plural "%d vulnerabilities dismissed"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
@@ -3930,6 +3935,9 @@ msgstr ""
msgid "Cluster cache cleared."
msgstr ""
+msgid "Cluster does not exist"
+msgstr ""
+
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr ""
@@ -6700,6 +6708,9 @@ msgstr ""
msgid "Difference between start date and now"
msgstr ""
+msgid "DiffsCompareBaseBranch|(HEAD)"
+msgstr ""
+
msgid "DiffsCompareBaseBranch|(base)"
msgstr ""
@@ -6823,12 +6834,20 @@ msgstr ""
msgid "Dismiss"
msgstr ""
+msgid "Dismiss %d selected vulnerability as"
+msgid_plural "Dismiss %d selected vulnerabilities as"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Dismiss DevOps Score introduction"
msgstr ""
msgid "Dismiss Merge Request promotion"
msgstr ""
+msgid "Dismiss Selected"
+msgstr ""
+
msgid "Dismiss Value Stream Analytics introduction box"
msgstr ""
@@ -7396,6 +7415,9 @@ msgstr ""
msgid "Environment"
msgstr ""
+msgid "Environment does not have deployments"
+msgstr ""
+
msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
msgstr ""
@@ -8293,6 +8315,9 @@ msgstr ""
msgid "Failure"
msgstr ""
+msgid "False positive"
+msgstr ""
+
msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged."
msgstr ""
@@ -12640,6 +12665,9 @@ msgstr ""
msgid "Name:"
msgstr ""
+msgid "Namespace is empty"
+msgstr ""
+
msgid "Namespace: %{namespace}"
msgstr ""
@@ -12897,9 +12925,6 @@ msgstr ""
msgid "No data to display"
msgstr ""
-msgid "No deployment platform available"
-msgstr ""
-
msgid "No deployments found"
msgstr ""
@@ -13149,6 +13174,9 @@ msgstr ""
msgid "NotificationEvent|Failed pipeline"
msgstr ""
+msgid "NotificationEvent|Fixed pipeline"
+msgstr ""
+
msgid "NotificationEvent|Merge merge request"
msgstr ""
@@ -17056,6 +17084,9 @@ msgstr ""
msgid "Security Reports|There was an error deleting the comment."
msgstr ""
+msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
msgid "Security Reports|There was an error dismissing the vulnerability."
msgstr ""
@@ -17212,6 +17243,9 @@ msgstr ""
msgid "Select a project to read Insights configuration file"
msgstr ""
+msgid "Select a reason"
+msgstr ""
+
msgid "Select a repository"
msgstr ""
@@ -22173,6 +22207,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
+msgid "Won't fix / Accept risk"
+msgstr ""
+
msgid "Work in progress Limit"
msgstr ""
@@ -22761,6 +22798,9 @@ msgstr ""
msgid "Zoom meeting removed"
msgstr ""
+msgid "[No reason]"
+msgstr ""
+
msgid "a deleted user"
msgstr ""
diff --git a/locale/unfound_translations.rb b/locale/unfound_translations.rb
index 1ae0958c43c..6f7934b4d65 100644
--- a/locale/unfound_translations.rb
+++ b/locale/unfound_translations.rb
@@ -14,4 +14,5 @@ N_('NotificationEvent|Close merge request')
N_('NotificationEvent|Reassign merge request')
N_('NotificationEvent|Merge merge request')
N_('NotificationEvent|Failed pipeline')
+N_('NotificationEvent|Fixed pipeline')
N_('NotificationEvent|New release')
diff --git a/spec/factories/ci/ref.rb b/spec/factories/ci/ref.rb
new file mode 100644
index 00000000000..891d8848a72
--- /dev/null
+++ b/spec/factories/ci/ref.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_ref, class: 'Ci::Ref' do
+ ref { 'master' }
+ status { :success }
+ tag { false }
+ project
+
+ before(:create) do |ref, evaluator|
+ next if ref.pipelines.exists?
+
+ ref.update!(last_updated_by_pipeline: create(:ci_pipeline, project: evaluator.project, ref: evaluator.ref, tag: evaluator.tag, status: evaluator.status))
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 1c72c54f0a1..561c0552007 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -356,7 +356,7 @@ describe 'Pipeline', :js do
end
end
- context 'test tabs' do
+ describe 'test tabs' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
before do
@@ -364,21 +364,31 @@ describe 'Pipeline', :js do
wait_for_requests
end
- it 'shows badge counter in Tests tab' do
- expect(pipeline.test_reports.total_count).to eq(4)
- expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s)
- end
+ context 'with test reports' do
+ it 'shows badge counter in Tests tab' do
+ expect(pipeline.test_reports.total_count).to eq(4)
+ expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s)
+ end
+
+ it 'does not call test_report.json endpoint by default', :js do
+ expect(page).to have_selector('.js-no-tests-to-show', visible: :all)
+ end
- it 'does not call test_report.json endpoint by default', :js do
- expect(page).to have_selector('.js-no-tests-to-show', visible: :all)
+ it 'does call test_report.json endpoint when tab is selected', :js do
+ find('.js-tests-tab-link').click
+ wait_for_requests
+
+ expect(page).to have_content('Test suites')
+ expect(page).to have_selector('.js-tests-detail', visible: :all)
+ end
end
- it 'does call test_report.json endpoint when tab is selected', :js do
- find('.js-tests-tab-link').click
- wait_for_requests
+ context 'without test reports' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
- expect(page).to have_content('Test suites')
- expect(page).to have_selector('.js-tests-detail', visible: :all)
+ it 'shows nothing' do
+ expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("")
+ end
end
end
diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/frontend/diffs/components/compare_versions_dropdown_spec.js
index e0686901483..5033bdd9044 100644
--- a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_dropdown_spec.js
@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue';
import diffsMockData from '../mock_data/merge_request_diffs';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { TEST_HOST } from 'helpers/test_constants';
const localVue = createLocalVue();
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
@@ -109,6 +110,24 @@ describe('CompareVersionsDropdown', () => {
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
expect(findLastLink().text()).toContain('(base)');
+ expect(findLastLink().text()).not.toContain('(HEAD)');
+ });
+
+ it('should render a correct head version link', () => {
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: { href: `${TEST_HOST}?diff_head=true` },
+ });
+
+ createComponent({
+ baseVersionPath,
+ otherVersions: diffsMockData.slice(1),
+ targetBranch,
+ });
+
+ expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
+ expect(findLastLink().text()).not.toContain('(base)');
+ expect(findLastLink().text()).toContain('(HEAD)');
});
it('should not render commits count if no showCommitsCount is passed', () => {
diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb
index 2384c87b377..d8dcce203fe 100644
--- a/spec/helpers/notifications_helper_spec.rb
+++ b/spec/helpers/notifications_helper_spec.rb
@@ -21,6 +21,7 @@ describe NotificationsHelper do
describe '#notification_event_name' do
it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') }
it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') }
+ it { expect(notification_event_name(:fixed_pipeline)).to match('Fixed pipeline') }
end
describe '#notification_icon_level' do
diff --git a/spec/lib/gitlab/checks/snippet_check_spec.rb b/spec/lib/gitlab/checks/snippet_check_spec.rb
index 7cb29debd1e..7cfcde7183f 100644
--- a/spec/lib/gitlab/checks/snippet_check_spec.rb
+++ b/spec/lib/gitlab/checks/snippet_check_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Checks::SnippetCheck do
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'raises an error' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can not create or delete branches.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
end
end
@@ -27,7 +27,7 @@ describe Gitlab::Checks::SnippetCheck do
let(:oldrev) { '0000000000000000000000000000000000000000' }
it 'raises an error' do
- expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can not create or delete branches.')
+ expect { subject.exec }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
end
end
end
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index de19db38176..a68ac8ee8fe 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::GitAccessSnippet do
let(:actor) { build(:deploy_key) }
it 'does not allow push and pull access' do
- expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:authentication_mechanism])
+ expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism])
end
end
@@ -76,8 +76,8 @@ describe Gitlab::GitAccessSnippet do
it 'blocks access when the user did not accept terms' do
message = /must accept the Terms of Service in order to perform this action/
- expect { push_access_check }.to raise_unauthorized(message)
- expect { pull_access_check }.to raise_unauthorized(message)
+ expect { push_access_check }.to raise_forbidden(message)
+ expect { pull_access_check }.to raise_forbidden(message)
end
it 'allows access when the user accepted the terms' do
@@ -101,13 +101,13 @@ describe Gitlab::GitAccessSnippet do
if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error
else
- expect { push_access_check }.to raise_error(described_class::UnauthorizedError)
+ expect { push_access_check }.to raise_error(described_class::ForbiddenError)
end
if Ability.allowed?(user, :read_snippet, snippet)
expect { pull_access_check }.not_to raise_error
else
- expect { pull_access_check }.to raise_error(described_class::UnauthorizedError)
+ expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
@@ -154,7 +154,7 @@ describe Gitlab::GitAccessSnippet do
with_them do
it "respects accessibility" do
- error_class = described_class::UnauthorizedError
+ error_class = described_class::ForbiddenError
if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error
@@ -180,7 +180,7 @@ describe Gitlab::GitAccessSnippet do
allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true)
- expect { push_access_check }.to raise_unauthorized(/You can't push code to a read-only GitLab instance/)
+ expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
end
end
@@ -198,10 +198,10 @@ describe Gitlab::GitAccessSnippet do
it 'raises error if SnippetCheck raises error' do
expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
- allow(check).to receive(:exec).and_raise(Gitlab::GitAccess::UnauthorizedError, 'foo')
+ allow(check).to receive(:exec).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo')
end
- expect { push_access_check }.to raise_unauthorized('foo')
+ expect { push_access_check }.to raise_forbidden('foo')
end
end
@@ -215,7 +215,7 @@ describe Gitlab::GitAccessSnippet do
raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
end
- def raise_unauthorized(message)
- raise_error(Gitlab::GitAccess::UnauthorizedError, message)
+ def raise_forbidden(message)
+ raise_error(Gitlab::GitAccess::ForbiddenError, message)
end
end
diff --git a/spec/lib/gitlab/graphql/docs/renderer_spec.rb b/spec/lib/gitlab/graphql/docs/renderer_spec.rb
new file mode 100644
index 00000000000..5ba70bb8f0a
--- /dev/null
+++ b/spec/lib/gitlab/graphql/docs/renderer_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Docs::Renderer do
+ describe '#contents' do
+ # Returns a Schema that uses the given `type`
+ def mock_schema(type)
+ query_type = Class.new(GraphQL::Schema::Object) do
+ graphql_name 'QueryType'
+
+ field :foo, type, null: true
+ end
+
+ GraphQL::Schema.define(query: query_type)
+ end
+
+ let_it_be(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/', 'default.md.haml') }
+
+ subject(:contents) do
+ described_class.new(
+ mock_schema(type).graphql_definition,
+ output_dir: nil,
+ template: template
+ ).contents
+ end
+
+ context 'A type with a field with a [Array] return type' do
+ let(:type) do
+ Class.new(GraphQL::Schema::Object) do
+ graphql_name 'ArrayTest'
+
+ field :foo, [GraphQL::STRING_TYPE], null: false, description: 'A description'
+ end
+ end
+
+ specify do
+ expectation = <<~DOC
+ ## ArrayTest
+
+ | Name | Type | Description |
+ | --- | ---- | ---------- |
+ | `foo` | String! => Array | A description |
+ DOC
+
+ is_expected.to include(expectation)
+ end
+ end
+
+ context 'A type with fields defined in reverse alphabetical order' do
+ let(:type) do
+ Class.new(GraphQL::Schema::Object) do
+ graphql_name 'OrderingTest'
+
+ field :foo, GraphQL::STRING_TYPE, null: false, description: 'A description of foo field'
+ field :bar, GraphQL::STRING_TYPE, null: false, description: 'A description of bar field'
+ end
+ end
+
+ specify do
+ expectation = <<~DOC
+ ## OrderingTest
+
+ | Name | Type | Description |
+ | --- | ---- | ---------- |
+ | `bar` | String! | A description of bar field |
+ | `foo` | String! | A description of foo field |
+ DOC
+
+ is_expected.to include(expectation)
+ end
+ end
+
+ context 'A type with a deprecated field' do
+ let(:type) do
+ Class.new(GraphQL::Schema::Object) do
+ graphql_name 'DeprecatedTest'
+
+ field :foo, GraphQL::STRING_TYPE, null: false, deprecation_reason: 'This is deprecated', description: 'A description'
+ end
+ end
+
+ specify do
+ expectation = <<~DOC
+ ## DeprecatedTest
+
+ | Name | Type | Description |
+ | --- | ---- | ---------- |
+ | `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated |
+ DOC
+
+ is_expected.to include(expectation)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index db45b9c42fd..d97d76cf35e 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -192,6 +192,7 @@ ci_pipelines:
- environments
- chat_data
- source_pipeline
+- ref_status
- source_bridge
- source_job
- sourced_pipelines
@@ -359,6 +360,7 @@ project:
- ci_pipelines
- all_pipelines
- stages
+- ci_refs
- builds
- runner_projects
- runners
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index f5a6ea4d5b0..2dd45d18ee9 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -89,6 +89,17 @@ describe Gitlab::IncomingEmail do
it 'does not match emails with extra bits' do
expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
end
+
+ context 'when a custom wildcard address is used' do
+ let(:wildcard_address) { 'custom.address+%{key}@example.com' }
+
+ it 'finds key if email matches address pattern' do
+ key = described_class.key_from_address(
+ 'custom.address+foo@example.com', wildcard_address: wildcard_address
+ )
+ expect(key).to eq('foo')
+ end
+ end
end
context 'self.key_from_fallback_message_id' do
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
new file mode 100644
index 00000000000..b6e47afc7e8
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::DuplicateJobs::Client, :clean_gitlab_redis_queues do
+ let(:worker_class) do
+ Class.new do
+ def self.name
+ 'TestDeduplicationWorker'
+ end
+
+ include ApplicationWorker
+
+ def perform(*args)
+ end
+ end
+ end
+
+ before do
+ stub_const('TestDeduplicationWorker', worker_class)
+ end
+
+ describe '#call' do
+ it 'adds a correct duplicate tag to the jobs', :aggregate_failures do
+ TestDeduplicationWorker.bulk_perform_async([['args1'], ['args2'], ['args1']])
+
+ job1, job2, job3 = TestDeduplicationWorker.jobs
+
+ expect(job1['duplicate-of']).to be_nil
+ expect(job2['duplicate-of']).to be_nil
+ expect(job3['duplicate-of']).to eq(job1['jid'])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
new file mode 100644
index 00000000000..2334439461e
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_redis_queues do
+ subject(:duplicate_job) do
+ described_class.new(job, queue)
+ end
+
+ let(:job) { { 'class' => 'AuthorizedProjectsWorker', 'args' => [1], 'jid' => '123' } }
+ let(:queue) { 'authorized_projects' }
+
+ let(:idempotency_key) do
+ hash = Digest::SHA256.hexdigest("#{job['class']}:#{job['args'].join('-')}")
+ "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:duplicate:#{queue}:#{hash}"
+ end
+
+ describe '#schedule' do
+ it 'calls schedule on the strategy' do
+ expect do |block|
+ expect_next_instance_of(Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecuting) do |strategy|
+ expect(strategy).to receive(:schedule).with(job, &block)
+ end
+
+ duplicate_job.schedule(&block)
+ end.to yield_control
+ end
+ end
+
+ describe '#perform' do
+ it 'calls perform on the strategy' do
+ expect do |block|
+ expect_next_instance_of(Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecuting) do |strategy|
+ expect(strategy).to receive(:perform).with(job, &block)
+ end
+
+ duplicate_job.perform(&block)
+ end.to yield_control
+ end
+ end
+
+ describe '#check!' do
+ context 'when there was no job in the queue yet' do
+ it { expect(duplicate_job.check!).to eq('123') }
+
+ it "adds a key with ttl set to #{described_class::DUPLICATE_KEY_TTL}" do
+ expect { duplicate_job.check! }
+ .to change { read_idempotency_key_with_ttl(idempotency_key) }
+ .from([nil, -2])
+ .to(['123', be_within(1).of(described_class::DUPLICATE_KEY_TTL)])
+ end
+ end
+
+ context 'when there was already a job with same arguments in the same queue' do
+ before do
+ set_idempotency_key(idempotency_key, 'existing-key')
+ end
+
+ it { expect(duplicate_job.check!).to eq('existing-key') }
+
+ it "does not change the existing key's TTL" do
+ expect { duplicate_job.check! }
+ .not_to change { read_idempotency_key_with_ttl(idempotency_key) }
+ .from(['existing-key', -1])
+ end
+
+ it 'sets the existing jid' do
+ duplicate_job.check!
+
+ expect(duplicate_job.existing_jid).to eq('existing-key')
+ end
+ end
+ end
+
+ describe '#delete!' do
+ context "when we didn't track the definition" do
+ it { expect { duplicate_job.delete! }.not_to raise_error }
+ end
+
+ context 'when the key exists in redis' do
+ before do
+ set_idempotency_key(idempotency_key, 'existing-key')
+ end
+
+ it 'removes the key from redis' do
+ expect { duplicate_job.delete! }
+ .to change { read_idempotency_key_with_ttl(idempotency_key) }
+ .from(['existing-key', -1])
+ .to([nil, -2])
+ end
+ end
+ end
+
+ describe '#duplicate?' do
+ it "raises an error if the check wasn't performed" do
+ expect { duplicate_job.duplicate? }.to raise_error /Call `#check!` first/
+ end
+
+ it 'returns false if the existing jid equals the job jid' do
+ duplicate_job.check!
+
+ expect(duplicate_job.duplicate?).to be(false)
+ end
+
+ it 'returns false if the existing jid is different from the job jid' do
+ set_idempotency_key(idempotency_key, 'a different jid')
+ duplicate_job.check!
+
+ expect(duplicate_job.duplicate?).to be(true)
+ end
+ end
+
+ def set_idempotency_key(key, value = '1')
+ Sidekiq.redis { |r| r.set(key, value) }
+ end
+
+ def read_idempotency_key_with_ttl(key)
+ Sidekiq.redis do |redis|
+ redis.pipelined do |p|
+ p.get(key)
+ p.ttl(key)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
new file mode 100644
index 00000000000..0ea248fbcf1
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/server_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::DuplicateJobs::Server, :clean_gitlab_redis_queues do
+ let(:worker_class) do
+ Class.new do
+ def self.name
+ 'TestDeduplicationWorker'
+ end
+
+ include ApplicationWorker
+
+ def perform(*args)
+ end
+ end
+ end
+
+ before do
+ stub_const('TestDeduplicationWorker', worker_class)
+ end
+
+ around do |example|
+ Sidekiq::Testing.inline! { example.run }
+ end
+
+ before(:context) do
+ Sidekiq::Testing.server_middleware do |chain|
+ chain.add described_class
+ end
+ end
+
+ after(:context) do
+ Sidekiq::Testing.server_middleware do |chain|
+ chain.remove described_class
+ end
+ end
+
+ describe '#call' do
+ it 'removes the stored job from redis' do
+ bare_job = { 'class' => 'TestDeduplicationWorker', 'args' => ['hello'] }
+ job_definition = Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob.new(bare_job.dup, 'test_deduplication')
+
+ expect(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob)
+ .to receive(:new).with(a_hash_including(bare_job), 'test_deduplication')
+ .and_return(job_definition).twice # once in client middleware
+ expect(job_definition).to receive(:delete!).and_call_original
+
+ TestDeduplicationWorker.perform_async('hello')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing_spec.rb
new file mode 100644
index 00000000000..f40e829f9a5
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies/until_executing_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecuting do
+ let(:fake_duplicate_job) do
+ instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob)
+ end
+
+ subject(:strategy) { described_class.new(fake_duplicate_job) }
+
+ describe '#schedule' do
+ it 'checks for duplicates before yielding' do
+ expect(fake_duplicate_job).to receive(:check!).ordered.and_return('a jid')
+ expect(fake_duplicate_job).to receive(:duplicate?).ordered.and_return(false)
+ expect { |b| strategy.schedule({}, &b) }.to yield_control
+ end
+
+ it 'adds the jid of the existing job to the job hash' do
+ allow(fake_duplicate_job).to receive(:check!).and_return('the jid')
+ job_hash = {}
+
+ expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
+ expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid')
+
+ strategy.schedule(job_hash) {}
+
+ expect(job_hash).to include('duplicate-of' => 'the jid')
+ end
+ end
+
+ describe '#perform' do
+ it 'deletes the lock before executing' do
+ expect(fake_duplicate_job).to receive(:delete!).ordered
+ expect { |b| strategy.perform({}, &b) }.to yield_control
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb
new file mode 100644
index 00000000000..6ecc2a3a5f8
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/strategies_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies do
+ describe '.for' do
+ it 'returns the right class for `until_executing`' do
+ expect(described_class.for(:until_executing)).to eq(described_class::UntilExecuting)
+ end
+
+ it 'raises an UnknownStrategyError when passing an unknown key' do
+ expect { described_class.for(:unknown) }.to raise_error(described_class::UnknownStrategyError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index 19242d25e27..2f325fd5052 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -46,7 +46,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
Gitlab::SidekiqMiddleware::WorkerContext::Server,
- Gitlab::SidekiqMiddleware::AdminMode::Server
+ Gitlab::SidekiqMiddleware::AdminMode::Server,
+ Gitlab::SidekiqMiddleware::DuplicateJobs::Server
]
end
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
@@ -117,7 +118,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::ClientMetrics,
Gitlab::SidekiqMiddleware::WorkerContext::Client,
Labkit::Middleware::Sidekiq::Client,
- Gitlab::SidekiqMiddleware::AdminMode::Client
+ Gitlab::SidekiqMiddleware::AdminMode::Client,
+ Gitlab::SidekiqMiddleware::DuplicateJobs::Client
]
end
diff --git a/spec/mailers/emails/pipelines_spec.rb b/spec/mailers/emails/pipelines_spec.rb
index 9996bd9a6c4..cc901da98dc 100644
--- a/spec/mailers/emails/pipelines_spec.rb
+++ b/spec/mailers/emails/pipelines_spec.rb
@@ -106,4 +106,17 @@ describe Emails::Pipelines do
let(:status_text) { 'Your pipeline has failed.' }
end
end
+
+ describe '#pipeline_fixed_email' do
+ subject { Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email)) }
+
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
+ let(:ref) { 'master' }
+ let(:sha) { project.commit(ref).sha }
+
+ it_behaves_like 'correct pipeline information' do
+ let(:status) { 'been fixed' }
+ let(:status_text) { 'Your pipeline has been fixed!' }
+ end
+ end
end
diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb
new file mode 100644
index 00000000000..aa3b8cdbc3e
--- /dev/null
+++ b/spec/models/ci/ref_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::Ref do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:last_updated_by_pipeline) }
+
+ it { is_expected.to validate_inclusion_of(:status).in_array(%w[success failed fixed]) }
+ it { is_expected.to validate_presence_of(:last_updated_by_pipeline) }
+end
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index f6a36dbb3fc..05aeafaa4d4 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -176,8 +176,20 @@ describe NotificationRecipient do
)
end
- before do
- notification_setting.update!(failed_pipeline: true)
+ it 'returns true' do
+ expect(recipient.suitable_notification_level?).to eq true
+ end
+ end
+
+ context "when action is fixed_pipeline" do
+ let(:recipient) do
+ described_class.new(
+ user,
+ :watch,
+ custom_action: :fixed_pipeline,
+ target: target,
+ project: project
+ )
end
it 'returns true' do
@@ -185,7 +197,7 @@ describe NotificationRecipient do
end
end
- context "when action is not failed_pipeline" do
+ context "when action is not fixed_pipeline or failed_pipeline" do
let(:recipient) do
described_class.new(
user,
@@ -196,10 +208,6 @@ describe NotificationRecipient do
)
end
- before do
- notification_setting.update!(success_pipeline: true)
- end
-
it 'returns false' do
expect(recipient.suitable_notification_level?).to eq false
end
@@ -309,6 +317,26 @@ describe NotificationRecipient do
expect(recipient.suitable_notification_level?).to eq false
end
end
+
+ context 'when custom_action is fixed_pipeline and success_pipeline event is enabled' do
+ let(:recipient) do
+ described_class.new(
+ user,
+ :watch,
+ custom_action: :fixed_pipeline,
+ target: target,
+ project: project
+ )
+ end
+
+ before do
+ notification_setting.update!(success_pipeline: true)
+ end
+
+ it 'returns true' do
+ expect(recipient.suitable_notification_level?).to eq true
+ end
+ end
end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 094c60e3e09..9ab9ae494ec 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -110,7 +110,8 @@ RSpec.describe NotificationSetting do
:reassign_merge_request,
:merge_merge_request,
:failed_pipeline,
- :success_pipeline
+ :success_pipeline,
+ :fixed_pipeline
)
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9b1c724f0c2..e7deae38b46 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -72,6 +72,7 @@ describe Project do
it { is_expected.to have_one(:project_setting) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:ci_pipelines) }
+ it { is_expected.to have_many(:ci_refs) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) }
diff --git a/spec/services/ci/update_ci_ref_status_service_spec.rb b/spec/services/ci/update_ci_ref_status_service_spec.rb
new file mode 100644
index 00000000000..2b069452a55
--- /dev/null
+++ b/spec/services/ci/update_ci_ref_status_service_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::UpdateCiRefStatusService do
+ describe '#call' do
+ subject { described_class.new(pipeline) }
+
+ shared_examples 'creates ci_ref' do
+ it 'creates a ci_ref with the pipeline attributes' do
+ expect do
+ expect(subject.call).to eq(true)
+ end.to change { Ci::Ref.count }.by(1)
+
+ created_ref = pipeline.reload.ref_status
+ %w[ref tag project status].each do |attr|
+ expect(created_ref[attr]).to eq(pipeline[attr])
+ end
+ end
+
+ it 'calls PipelineNotificationWorker pasing the ref_status' do
+ expect(PipelineNotificationWorker).to receive(:perform_async).with(pipeline.id, ref_status: pipeline.status)
+
+ subject.call
+ end
+ end
+
+ shared_examples 'updates ci_ref' do
+ where(:ref_status, :pipeline_status, :next_status) do
+ [
+ %w[failed success fixed],
+ %w[failed failed failed],
+ %w[success success success],
+ %w[success failed failed]
+ ]
+ end
+
+ with_them do
+ let(:ci_ref) { create(:ci_ref, status: ref_status) }
+ let(:pipeline) { create(:ci_pipeline, status: pipeline_status, project: ci_ref.project, ref: ci_ref.ref) }
+
+ it 'sets ci_ref.status to next_status' do
+ expect do
+ expect(subject.call).to eq(true)
+ expect(ci_ref.reload.status).to eq(next_status)
+ end.not_to change { Ci::Ref.count }
+ end
+
+ it 'calls PipelineNotificationWorker pasing the ref_status' do
+ expect(PipelineNotificationWorker).to receive(:perform_async).with(pipeline.id, ref_status: next_status)
+
+ subject.call
+ end
+ end
+ end
+
+ shared_examples 'does a noop' do
+ it "doesn't change ci_ref" do
+ expect do
+ expect do
+ expect(subject.call).to eq(false)
+ end.not_to change { ci_ref.reload.status }
+ end.not_to change { Ci::Ref.count }
+ end
+
+ it "doesn't call PipelineNotificationWorker" do
+ expect(PipelineNotificationWorker).not_to receive(:perform_async)
+
+ subject.call
+ end
+ end
+
+ context "ci_ref doesn't exists" do
+ let(:pipeline) { create(:ci_pipeline, :success, ref: 'new-ref') }
+
+ it_behaves_like 'creates ci_ref'
+
+ context 'when an ActiveRecord::RecordNotUnique validation is raised' do
+ let(:ci_ref) { create(:ci_ref, status: 'failed') }
+ let(:pipeline) { create(:ci_pipeline, status: :success, project: ci_ref.project, ref: ci_ref.ref) }
+
+ it 'reloads the ci_ref and retries once' do
+ subject.instance_variable_set("@ref", subject.send(:build_ref))
+
+ expect do
+ expect(subject.call).to eq(true)
+ end.not_to change { Ci::Ref.count }
+ expect(ci_ref.reload.status).to eq('fixed')
+ end
+
+ it 'raises error on multiple retries' do
+ allow_any_instance_of(Ci::Ref).to receive(:update)
+ .and_raise(ActiveRecord::RecordNotUnique)
+
+ expect { subject.call }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
+ end
+ end
+
+ context 'ci_ref exists' do
+ let!(:ci_ref) { create(:ci_ref, status: 'failed') }
+ let(:pipeline) { ci_ref.pipelines.first }
+
+ it_behaves_like 'updates ci_ref'
+
+ context 'pipeline status is invalid' do
+ let!(:pipeline) { create(:ci_pipeline, :running, project: ci_ref.project, ref: ci_ref.ref, tag: ci_ref.tag) }
+
+ it_behaves_like 'does a noop'
+ end
+
+ context 'newer pipeline finished' do
+ let(:newer_pipeline) { create(:ci_pipeline, :success, project: ci_ref.project, ref: ci_ref.ref, tag: ci_ref.tag) }
+
+ before do
+ ci_ref.update!(last_updated_by_pipeline: newer_pipeline)
+ end
+
+ it_behaves_like 'does a noop'
+ end
+
+ context 'ref is stale' do
+ let(:pipeline1) { create(:ci_pipeline, :success, project: ci_ref.project, ref: ci_ref.ref, tag: ci_ref.tag) }
+ let(:pipeline2) { create(:ci_pipeline, :success, project: ci_ref.project, ref: ci_ref.ref, tag: ci_ref.tag) }
+
+ it 'reloads the ref and retry' do
+ service1 = described_class.new(pipeline1)
+ service2 = described_class.new(pipeline2)
+
+ service2.send(:ref)
+ service1.call
+ expect(ci_ref.reload.status).to eq('fixed')
+ expect do
+ expect(service2.call).to eq(true)
+ # We expect 'success' in this case rather than 'fixed' because
+ # the ref is correctly reloaded on stale error.
+ expect(ci_ref.reload.status).to eq('success')
+ end.not_to change { Ci::Ref.count }
+ end
+
+ it 'aborts when a newer pipeline finished' do
+ service1 = described_class.new(pipeline1)
+ service2 = described_class.new(pipeline2)
+
+ service2.call
+ expect do
+ expect(service1.call).to eq(false)
+ expect(ci_ref.reload.status).to eq('fixed')
+ end.not_to change { Ci::Ref.count }
+ end
+ end
+
+ context 'ref exists as both tag/branch and tag' do
+ let(:pipeline) { create(:ci_pipeline, :failed, project: ci_ref.project, ref: ci_ref.ref, tag: true) }
+ let!(:branch_pipeline) { create(:ci_pipeline, :success, project: ci_ref.project, ref: ci_ref.ref, tag: false) }
+
+ it_behaves_like 'creates ci_ref'
+ end
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 07a1be6c12b..120bfc6d0ca 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2315,6 +2315,7 @@ describe NotificationService, :mailer do
user = create_user_with_notification(:custom, 'custom_enabled')
update_custom_notification(:success_pipeline, user, resource: project)
update_custom_notification(:failed_pipeline, user, resource: project)
+ update_custom_notification(:fixed_pipeline, user, resource: project)
user
end
@@ -2322,6 +2323,7 @@ describe NotificationService, :mailer do
user = create_user_with_notification(:custom, 'custom_disabled')
update_custom_notification(:success_pipeline, user, resource: project, value: false)
update_custom_notification(:failed_pipeline, user, resource: project, value: false)
+ update_custom_notification(:fixed_pipeline, user, resource: project, value: false)
user
end
@@ -2514,6 +2516,85 @@ describe NotificationService, :mailer do
end
end
end
+
+ context 'with a fixed pipeline' do
+ let(:ref_status) { 'fixed' }
+
+ context 'when the creator has no custom notification set' do
+ let(:pipeline) { create_pipeline(u_member, :success) }
+
+ it 'emails only the creator' do
+ notification.pipeline_finished(pipeline, ref_status: ref_status)
+
+ should_only_email(u_member, kind: :bcc)
+ end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { pipeline }
+ let(:notification_trigger) { notification.pipeline_finished(pipeline, ref_status: ref_status) }
+ end
+
+ context 'when the creator has group notification email set' do
+ let(:group_notification_email) { 'user+group@example.com' }
+
+ before do
+ group = create(:group)
+
+ project.update(group: group)
+ create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
+ end
+
+ it 'sends to group notification email' do
+ notification.pipeline_finished(pipeline, ref_status: ref_status)
+
+ expect(email_recipients(kind: :bcc).first).to eq(group_notification_email)
+ end
+ end
+ end
+
+ context 'when the creator has watch set' do
+ before do
+ pipeline = create_pipeline(u_watcher, :success)
+ notification.pipeline_finished(pipeline, ref_status: ref_status)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_watcher, kind: :bcc)
+ end
+ end
+
+ context 'when the creator has custom notifications, but without any set' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_unset, :success)
+ notification.pipeline_finished(pipeline, ref_status: ref_status)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_custom_notification_unset, kind: :bcc)
+ end
+ end
+
+ context 'when the creator has custom notifications disabled' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_disabled, :success)
+ notification.pipeline_finished(pipeline, ref_status: ref_status)
+ end
+
+ it 'notifies nobody' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when the creator has custom notifications set' do
+ it 'emails only the creator' do
+ pipeline = create_pipeline(u_custom_notification_enabled, :success)
+
+ notification.pipeline_finished(pipeline, ref_status: ref_status)
+
+ should_only_email(u_custom_notification_enabled, kind: :bcc)
+ end
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
index 60c0ec45c71..8f5bfdacc3a 100644
--- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
@@ -65,9 +65,16 @@ shared_examples 'resource notes mentions migration' do |migration_class, resourc
end
shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes|
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+ end
+
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
Timecop.freeze do
+ resource_count = is_for_notes ? Note.count : resource_class.count
+ expect(resource_count).to eq 5
+
migrate!
migration = described_class::MIGRATION
diff --git a/spec/support/shared_examples/views/pipeline_status_changes_email.rb b/spec/support/shared_examples/views/pipeline_status_changes_email.rb
new file mode 100644
index 00000000000..15b4ce9c44e
--- /dev/null
+++ b/spec/support/shared_examples/views/pipeline_status_changes_email.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+shared_examples 'pipeline status changes email' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user, developer_projects: [project]) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha,
+ status: status)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+ assign(:merge_request, merge_request)
+ end
+
+ shared_examples_for 'renders the pipeline status changes email correctly' do
+ context 'pipeline with user' do
+ it 'renders the email correctly' do
+ render
+
+ expect(rendered).to have_content title
+ expect(rendered).to have_content pipeline.project.name
+ expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
+ expect(rendered).to have_content pipeline.commit.author_name
+ expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content pipeline.user.name
+
+ if status == :failed
+ expect(rendered).to have_content build.name
+ end
+ end
+
+ it_behaves_like 'correct pipeline information for pipelines for merge requests'
+ end
+
+ context 'pipeline without user' do
+ before do
+ pipeline.update_attribute(:user, nil)
+ end
+
+ it 'renders the email correctly' do
+ render
+
+ expect(rendered).to have_content title
+ expect(rendered).to have_content pipeline.project.name
+ expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
+ expect(rendered).to have_content pipeline.commit.author_name
+ expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content "by API"
+
+ if status == :failed
+ expect(rendered).to have_content build.name
+ end
+ end
+ end
+ end
+
+ context 'when the pipeline contains a failed job' do
+ let!(:build) { create(:ci_build, status: status, pipeline: pipeline, project: pipeline.project) }
+
+ it_behaves_like 'renders the pipeline status changes email correctly'
+ end
+
+ context 'when the latest failed job is a bridge job' do
+ let!(:build) { create(:ci_bridge, status: status, pipeline: pipeline, project: pipeline.project) }
+
+ it_behaves_like 'renders the pipeline status changes email correctly'
+ end
+end
diff --git a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
index a540a53c91d..80dc14b523d 100644
--- a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
@@ -3,72 +3,8 @@
require 'spec_helper'
describe 'notify/pipeline_failed_email.html.haml' do
- include Devise::Test::ControllerHelpers
-
- let(:user) { create(:user, developer_projects: [project]) }
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, :simple, source_project: project) }
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- user: user,
- ref: project.default_branch,
- sha: project.commit.sha,
- status: :failed)
- end
-
- before do
- assign(:project, project)
- assign(:pipeline, pipeline)
- assign(:merge_request, merge_request)
- end
-
- shared_examples_for 'renders the pipeline failed email correctly' do
- context 'pipeline with user' do
- it 'renders the email correctly' do
- render
-
- expect(rendered).to have_content "Your pipeline has failed"
- expect(rendered).to have_content pipeline.project.name
- expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
- expect(rendered).to have_content pipeline.commit.author_name
- expect(rendered).to have_content "##{pipeline.id}"
- expect(rendered).to have_content pipeline.user.name
- expect(rendered).to have_content build.name
- end
-
- it_behaves_like 'correct pipeline information for pipelines for merge requests'
- end
-
- context 'pipeline without user' do
- before do
- pipeline.update_attribute(:user, nil)
- end
-
- it 'renders the email correctly' do
- render
-
- expect(rendered).to have_content "Your pipeline has failed"
- expect(rendered).to have_content pipeline.project.name
- expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
- expect(rendered).to have_content pipeline.commit.author_name
- expect(rendered).to have_content "##{pipeline.id}"
- expect(rendered).to have_content "by API"
- expect(rendered).to have_content build.name
- end
- end
- end
-
- context 'when the pipeline contains a failed job' do
- let!(:build) { create(:ci_build, :failed, pipeline: pipeline, project: pipeline.project) }
-
- it_behaves_like 'renders the pipeline failed email correctly'
- end
-
- context 'when the latest failed job is a bridge job' do
- let!(:build) { create(:ci_bridge, status: :failed, pipeline: pipeline, project: pipeline.project) }
-
- it_behaves_like 'renders the pipeline failed email correctly'
+ it_behaves_like 'pipeline status changes email' do
+ let(:title) { 'Your pipeline has failed' }
+ let(:status) { :failed }
end
end
diff --git a/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb b/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb
new file mode 100644
index 00000000000..382fc5ecdd3
--- /dev/null
+++ b/spec/views/notify/pipeline_fixed_email.html.haml_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'notify/pipeline_fixed_email.html.haml' do
+ it_behaves_like 'pipeline status changes email' do
+ let(:title) { 'Your pipeline has been fixed!' }
+ let(:status) { :success }
+ end
+end
diff --git a/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb b/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb
new file mode 100644
index 00000000000..ec540dc3f77
--- /dev/null
+++ b/spec/views/notify/pipeline_fixed_email.text.erb_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'notify/pipeline_fixed_email.text.erb' do
+ it_behaves_like 'pipeline status changes email' do
+ let(:title) { 'Your pipeline has been fixed!' }
+ let(:status) { :success }
+ end
+end
diff --git a/spec/views/notify/pipeline_success_email.html.haml_spec.rb b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
index fbf33b7ec35..417909fd67b 100644
--- a/spec/views/notify/pipeline_success_email.html.haml_spec.rb
+++ b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
@@ -3,56 +3,8 @@
require 'spec_helper'
describe 'notify/pipeline_success_email.html.haml' do
- include Devise::Test::ControllerHelpers
-
- let(:user) { create(:user, developer_projects: [project]) }
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, :simple, source_project: project) }
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- user: user,
- ref: project.default_branch,
- sha: project.commit.sha,
- status: :success)
- end
-
- before do
- assign(:project, project)
- assign(:pipeline, pipeline)
- assign(:merge_request, merge_request)
- end
-
- context 'pipeline with user' do
- it 'renders the email correctly' do
- render
-
- expect(rendered).to have_content "Your pipeline has passed"
- expect(rendered).to have_content pipeline.project.name
- expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
- expect(rendered).to have_content pipeline.commit.author_name
- expect(rendered).to have_content "##{pipeline.id}"
- expect(rendered).to have_content pipeline.user.name
- end
-
- it_behaves_like 'correct pipeline information for pipelines for merge requests'
- end
-
- context 'pipeline without user' do
- before do
- pipeline.update_attribute(:user, nil)
- end
-
- it 'renders the email correctly' do
- render
-
- expect(rendered).to have_content "Your pipeline has passed"
- expect(rendered).to have_content pipeline.project.name
- expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
- expect(rendered).to have_content pipeline.commit.author_name
- expect(rendered).to have_content "##{pipeline.id}"
- expect(rendered).to have_content "by API"
- end
+ it_behaves_like 'pipeline status changes email' do
+ let(:title) { 'Your pipeline has passed' }
+ let(:status) { :success }
end
end
diff --git a/spec/views/notify/pipeline_success_email.text.erb_spec.rb b/spec/views/notify/pipeline_success_email.text.erb_spec.rb
index ba4633bc346..4a914cab85e 100644
--- a/spec/views/notify/pipeline_success_email.text.erb_spec.rb
+++ b/spec/views/notify/pipeline_success_email.text.erb_spec.rb
@@ -3,24 +3,8 @@
require 'spec_helper'
describe 'notify/pipeline_success_email.text.erb' do
- let(:user) { create(:user, developer_projects: [project]) }
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, :simple, source_project: project) }
-
- let(:pipeline) do
- create(:ci_pipeline,
- :success,
- project: project,
- user: user,
- ref: project.default_branch,
- sha: project.commit.sha)
- end
-
- before do
- assign(:project, project)
- assign(:pipeline, pipeline)
- assign(:merge_request, merge_request)
+ it_behaves_like 'pipeline status changes email' do
+ let(:title) { 'Your pipeline has passed' }
+ let(:status) { :success }
end
-
- it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
index 98b0f139fe2..5defd3d5bd7 100644
--- a/spec/workers/pipeline_notification_worker_spec.rb
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -3,13 +3,16 @@
require 'spec_helper'
describe PipelineNotificationWorker, :mailer do
- let(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:pipeline) { create(:ci_pipeline) }
describe '#execute' do
it 'calls NotificationService#pipeline_finished when the pipeline exists' do
- expect(NotificationService).to receive_message_chain(:new, :pipeline_finished)
+ notification_service_double = double
+ expect(notification_service_double).to receive(:pipeline_finished)
+ .with(pipeline, ref_status: 'success', recipients: ['test@gitlab.com'])
+ expect(NotificationService).to receive(:new).and_return(notification_service_double)
- subject.perform(pipeline.id)
+ subject.perform(pipeline.id, ref_status: 'success', recipients: ['test@gitlab.com'])
end
it 'does nothing when the pipeline does not exist' do
diff --git a/spec/workers/pipeline_update_ci_ref_status_worker_service_spec.rb b/spec/workers/pipeline_update_ci_ref_status_worker_service_spec.rb
new file mode 100644
index 00000000000..7228de4f895
--- /dev/null
+++ b/spec/workers/pipeline_update_ci_ref_status_worker_service_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PipelineUpdateCiRefStatusWorker do
+ let(:worker) { described_class.new }
+ let(:pipeline) { create(:ci_pipeline) }
+
+ describe '#perform' do
+ it 'updates the ci_ref status' do
+ expect(Ci::UpdateCiRefStatusService).to receive(:new)
+ .with(pipeline)
+ .and_return(double(call: true))
+
+ worker.perform(pipeline.id)
+ end
+ end
+end