Commit e0fa0638 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent f8d15ca6
...@@ -146,6 +146,7 @@ linters: ...@@ -146,6 +146,7 @@ linters:
- 'app/views/notify/_failed_builds.html.haml' - 'app/views/notify/_failed_builds.html.haml'
- 'app/views/notify/_reassigned_issuable_email.html.haml' - 'app/views/notify/_reassigned_issuable_email.html.haml'
- 'app/views/notify/_removal_notification.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/autodevops_disabled_email.html.haml'
- 'app/views/notify/changed_milestone_email.html.haml' - 'app/views/notify/changed_milestone_email.html.haml'
- 'app/views/notify/import_issues_csv_email.html.haml' - 'app/views/notify/import_issues_csv_email.html.haml'
...@@ -163,7 +164,6 @@ linters: ...@@ -163,7 +164,6 @@ linters:
- 'app/views/notify/pages_domain_verification_failed_email.html.haml' - 'app/views/notify/pages_domain_verification_failed_email.html.haml'
- 'app/views/notify/pages_domain_verification_succeeded_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_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_exported_email.html.haml'
- 'app/views/notify/project_was_moved_email.html.haml' - 'app/views/notify/project_was_moved_email.html.haml'
- 'app/views/notify/project_was_not_exported_email.html.haml' - 'app/views/notify/project_was_not_exported_email.html.haml'
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { n__, __, sprintf } from '~/locale'; import { n__, __, sprintf } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
...@@ -94,6 +95,9 @@ export default { ...@@ -94,6 +95,9 @@ export default {
} }
return version.versionIndex === -1; return version.versionIndex === -1;
}, },
isHead() {
return parseBoolean(getParameterByName('diff_head'));
},
isLatest(version) { isLatest(version) {
return ( return (
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
...@@ -121,7 +125,8 @@ export default { ...@@ -121,7 +125,8 @@ export default {
<div> <div>
<strong> <strong>
{{ versionName(version) }} {{ versionName(version) }}
<template v-if="isBase(version)">{{ <template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
<template v-else-if="isBase(version)">{{
s__('DiffsCompareBaseBranch|(base)') s__('DiffsCompareBaseBranch|(base)')
}}</template> }}</template>
</strong> </strong>
......
...@@ -134,6 +134,10 @@ export default () => { ...@@ -134,6 +134,10 @@ export default () => {
axios axios
.get(dataset.testReportsCountEndpoint) .get(dataset.testReportsCountEndpoint)
.then(({ data }) => { .then(({ data }) => {
if (!data.total_count) {
return;
}
document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
}) })
.catch(() => {}); .catch(() => {});
......
<script> <script>
import _ from 'underscore'; import { escape as esc } from 'lodash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
...@@ -43,10 +43,10 @@ You are going to change the username %{currentUsernameBold} to %{newUsernameBold ...@@ -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. 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.`), Please update your Git repository remotes as soon as possible.`),
{ {
currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`, currentUsernameBold: `<strong>${esc(this.username)}</strong>`,
newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`, newUsernameBold: `<strong>${esc(this.newUsername)}</strong>`,
currentUsername: _.escape(this.username), currentUsername: esc(this.username),
newUsername: _.escape(this.newUsername), newUsername: esc(this.newUsername),
}, },
false, false,
); );
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import 'cropper'; import 'cropper';
import _ from 'underscore'; import { isString } from 'lodash';
(() => { (() => {
// Matches everything but the file name // Matches everything but the file name
...@@ -29,7 +29,7 @@ import _ from 'underscore'; ...@@ -29,7 +29,7 @@ import _ from 'underscore';
this.onModalShow = this.onModalShow.bind(this); this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this); this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input); this.fileInput = $(input);
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; this.modalCropImg = isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput this.fileInput
.attr('name', `${this.fileInput.attr('name')}-trigger`) .attr('name', `${this.fileInput.attr('name')}-trigger`)
.attr('id', `${this.fileInput.attr('id')}-trigger`); .attr('id', `${this.fileInput.attr('id')}-trigger`);
...@@ -47,9 +47,9 @@ import _ from 'underscore'; ...@@ -47,9 +47,9 @@ import _ from 'underscore';
this.filename = this.getElement(filename); this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage); this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl); this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop; this.modalCrop = isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; this.uploadImageBtn = isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg; this.modalCropImg = isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents(); this.bindEvents();
} }
......
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
// Classes using mixins coming from @gitlab-ui // Classes using mixins coming from @gitlab-ui
// can be removed once https://gitlab.com/gitlab-org/gitlab/merge_requests/19021 has been merged // 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-red-100 { @include gl-bg-red-100; }
.gl-bg-orange-100 { @include gl-bg-orange-100; } .gl-bg-orange-100 { @include gl-bg-orange-100; }
.gl-bg-gray-100 { @include gl-bg-gray-100; } .gl-bg-gray-100 { @include gl-bg-gray-100; }
......
...@@ -10,6 +10,10 @@ module Emails ...@@ -10,6 +10,10 @@ module Emails
pipeline_mail(pipeline, recipients, 'failed') pipeline_mail(pipeline, recipients, 'failed')
end end
def pipeline_fixed_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'been fixed')
end
private private
def pipeline_mail(pipeline, recipients, status) def pipeline_mail(pipeline, recipients, status)
......
...@@ -145,6 +145,10 @@ class NotifyPreview < ActionMailer::Preview ...@@ -145,6 +145,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end end
def pipeline_fixed_email
Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email))
end
def autodevops_disabled_email def autodevops_disabled_email
Notify.autodevops_disabled_email(pipeline, user.email).message Notify.autodevops_disabled_email(pipeline, user.email).message
end end
......
...@@ -63,6 +63,14 @@ module Ci ...@@ -63,6 +63,14 @@ module Ci
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id 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 :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_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
...@@ -227,7 +235,7 @@ module Ci ...@@ -227,7 +235,7 @@ module Ci
after_transition any => [:success, :failed] do |pipeline| after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id) PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id)
end end
end end
......
# 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
...@@ -52,7 +52,8 @@ class NotificationRecipient ...@@ -52,7 +52,8 @@ class NotificationRecipient
when :mention when :mention
@type == :mention @type == :mention
when :participating 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 when :custom
custom_enabled? || %i[participating mention].include?(@type) custom_enabled? || %i[participating mention].include?(@type)
when :watch when :watch
...@@ -63,7 +64,13 @@ class NotificationRecipient ...@@ -63,7 +64,13 @@ class NotificationRecipient
end end
def custom_enabled? 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 end
def unsubscribed? def unsubscribed?
......
...@@ -44,6 +44,7 @@ class NotificationSetting < ApplicationRecord ...@@ -44,6 +44,7 @@ class NotificationSetting < ApplicationRecord
:reassign_merge_request, :reassign_merge_request,
:merge_merge_request, :merge_merge_request,
:failed_pipeline, :failed_pipeline,
:fixed_pipeline,
:success_pipeline :success_pipeline
].freeze ].freeze
...@@ -76,9 +77,9 @@ class NotificationSetting < ApplicationRecord ...@@ -76,9 +77,9 @@ class NotificationSetting < ApplicationRecord
setting setting
end end
# Allow people to receive failed pipeline notifications if they already have # Allow people to receive both failed pipeline/fixed pipeline notifications
# custom notifications enabled, as these are more like mentions than the other # if they already have custom notifications enabled,
# custom settings. # as these are more like mentions than the other custom settings.
def failed_pipeline def failed_pipeline
bool = super bool = super
...@@ -86,6 +87,13 @@ class NotificationSetting < ApplicationRecord ...@@ -86,6 +87,13 @@ class NotificationSetting < ApplicationRecord
end end
alias_method :failed_pipeline?, :failed_pipeline 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) def event_enabled?(event)
respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend
end end
......
...@@ -267,6 +267,7 @@ class Project < ApplicationRecord ...@@ -267,6 +267,7 @@ class Project < ApplicationRecord
class_name: 'Ci::Pipeline', class_name: 'Ci::Pipeline',
inverse_of: :project inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', 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 # 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 # build traces. Currently there's no efficient way of removing this data in
......
...@@ -49,7 +49,7 @@ class PipelinesEmailService < Service ...@@ -49,7 +49,7 @@ class PipelinesEmailService < Service
return unless all_recipients.any? return unless all_recipients.any?
pipeline_id = data[:object_attributes][:id] pipeline_id = data[:object_attributes][:id]
PipelineNotificationWorker.new.perform(pipeline_id, all_recipients) PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients)
end end
def can_test? def can_test?
......
# 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
...@@ -9,15 +9,10 @@ module MergeRequests ...@@ -9,15 +9,10 @@ module MergeRequests
end end
def create_detached_merge_request_pipeline(merge_request) def create_detached_merge_request_pipeline(merge_request)
if can_use_merge_request_ref?(merge_request) Ci::CreatePipelineService.new(merge_request.source_project,
Ci::CreatePipelineService.new(merge_request.source_project, current_user, current_user,
ref: merge_request.ref_path) ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request))
.execute(:merge_request_event, merge_request: merge_request) .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
end end
def can_create_pipeline_for?(merge_request) def can_create_pipeline_for?(merge_request)
...@@ -33,6 +28,16 @@ module MergeRequests ...@@ -33,6 +28,16 @@ module MergeRequests
def allow_duplicate def allow_duplicate
params[:allow_duplicate] params[:allow_duplicate]
end 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
end end
......
...@@ -434,18 +434,19 @@ class NotificationService ...@@ -434,18 +434,19 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end 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 # Must always check project configuration since recipients could be a list of emails
# from the PipelinesEmailService integration. # from the PipelinesEmailService integration.
return if pipeline.project.emails_disabled? 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) return unless mailer.respond_to?(email_template)
recipients ||= notifiable_users( recipients ||= notifiable_users(
[pipeline.user], :watch, [pipeline.user], :watch,
custom_action: :"#{pipeline.status}_pipeline", custom_action: :"#{ref_status}_pipeline",
target: pipeline target: pipeline
).map do |user| ).map do |user|
user.notification_email_for(pipeline.project.group) user.notification_email_for(pipeline.project.group)
......
- 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)}.
<%= 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) %>.
= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!'
<%= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!' -%>
%tr.table-success = render 'notify/successful_pipeline', title: 'Your pipeline has passed.'
%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)}.
Your pipeline has passed. <%= render 'notify/successful_pipeline', title: '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) %>.
...@@ -668,6 +668,13 @@ ...@@ -668,6 +668,13 @@
:resource_boundary: :cpu :resource_boundary: :cpu
:weight: 3 :weight: 3
:idempotent: :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 - :name: pipeline_hooks:build_hooks
:feature_category: :continuous_integration :feature_category: :continuous_integration
:has_external_dependencies: :has_external_dependencies:
......
...@@ -8,12 +8,20 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -8,12 +8,20 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :cpu worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id, recipients = nil) def perform(pipeline_id, args = {})
pipeline = Ci::Pipeline.find_by(id: pipeline_id) 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 return unless pipeline
NotificationService.new.pipeline_finished(pipeline, recipients) NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
# 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
---
title: Fix Insights displaying JSON on back navigation
merge_request: 25801
author:
type: fixed
---
title: Fix "Add an epic" form
merge_request: 26003
author:
type: fixed
---
title: Notifications for when pipelines are fixed
merge_request: 16951
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Display base label in versions drop down
merge_request: 25834
author:
type: added
...@@ -244,6 +244,12 @@ Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci ...@@ -244,6 +244,12 @@ Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci
Settings['incoming_email'] ||= Settingslogic.new({}) Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? 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 # Build Artifacts
# #
......
# 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
# frozen_string_literal: true
class AddFixedPipelineToNotificationSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :notification_settings, :fixed_pipeline, :boolean
end
end
# 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
# 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
# frozen_string_literal: true # frozen_string_literal: true
class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2] class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
disable_ddl_transaction! disable_ddl_transaction!
...@@ -26,7 +28,7 @@ class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2] ...@@ -26,7 +28,7 @@ class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
.where(QUERY_CONDITIONS) .where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index| .each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first 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
end end
......
# 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
# 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
# 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
# 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
# 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
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -878,6 +878,17 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do ...@@ -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" t.index ["pipeline_id"], name: "index_ci_pipelines_config_on_pipeline_id"
end 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| create_table "ci_resource_groups", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
...@@ -2845,6 +2856,7 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do ...@@ -2845,6 +2856,7 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.boolean "issue_due" t.boolean "issue_due"
t.boolean "new_epic" t.boolean "new_epic"
t.string "notification_email" t.string "notification_email"
t.boolean "fixed_pipeline"
t.boolean "new_release" t.boolean "new_release"
t.index ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type" 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 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 ...@@ -4356,8 +4368,6 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.text "description_html" t.text "description_html"
t.bigint "start_date_sourcing_milestone_id" t.bigint "start_date_sourcing_milestone_id"
t.bigint "due_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 "state", limit: 2, default: 1, null: false
t.integer "severity", limit: 2, null: false t.integer "severity", limit: 2, null: false
t.boolean "severity_overridden", default: false t.boolean "severity_overridden", default: false
...@@ -4369,9 +4379,11 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do ...@@ -4369,9 +4379,11 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
t.bigint "confirmed_by_id" t.bigint "confirmed_by_id"
t.datetime_with_timezone "confirmed_at" 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 ["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 ["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 ["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 ["epic_id"], name: "index_vulnerabilities_on_epic_id"
t.index ["last_edited_by_id"], name: "index_vulnerabilities_on_last_edited_by_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 ...@@ -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", "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", "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_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_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_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 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 ...@@ -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", "milestones", name: "fk_131d289c65", on_delete: :nullify
add_foreign_key "vulnerabilities", "projects", name: "fk_efb96ab1e2", on_delete: :cascade 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: "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: "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: "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: "resolved_by_id", name: "fk_76bc5f5455", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "updated_by_id", name: "fk_7ac31eacb9", on_delete: :nullify add_foreign_key "vulnerabilities", "users", column: "updated_by_id", name: "fk_7ac31eacb9", on_delete: :nullify
......
...@@ -1862,6 +1862,12 @@ type Epic implements Noteable { ...@@ -1862,6 +1862,12 @@ type Epic implements Noteable {
""" """
descendantCounts: EpicDescendantCount 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 of the epic
""" """
...@@ -2178,6 +2184,21 @@ type EpicDescendantCount { ...@@ -2178,6 +2184,21 @@ type EpicDescendantCount {
openedIssues: Int openedIssues: Int
} }
"""
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. An edge in a connection.
""" """
......
...@@ -13,6 +13,9 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -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 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. fields and methods on a model are available via GraphQL.
CAUTION: **Caution:**
Fields that are deprecated are marked with **{warning-solid}**.
## AddAwardEmojiPayload ## AddAwardEmojiPayload
Autogenerated return type of AddAwardEmoji Autogenerated return type of AddAwardEmoji
...@@ -69,7 +72,7 @@ Represents a project or group board ...@@ -69,7 +72,7 @@ Represents a project or group board
| `authoredDate` | Time | Timestamp of when the commit was authored | | `authoredDate` | Time | Timestamp of when the commit was authored |
| `description` | String | Description of the commit message | | `description` | String | Description of the commit message |
| `id` | ID! | ID (global ID) of the commit | | `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 | | `message` | String | Raw commit message |
| `sha` | String! | SHA1 ID of the commit | | `sha` | String! | SHA1 ID of the commit |
| `signatureHtml` | String | Rendered HTML of the commit signature | | `signatureHtml` | String | Rendered HTML of the commit signature |
...@@ -294,6 +297,7 @@ Represents an epic. ...@@ -294,6 +297,7 @@ Represents an epic.
| `closedAt` | Time | Timestamp of the epic's closure | | `closedAt` | Time | Timestamp of the epic's closure |
| `createdAt` | Time | Timestamp of the epic's creation | | `createdAt` | Time | Timestamp of the epic's creation |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues | | `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 | | `description` | String | Description of the epic |
| `downvotes` | Int! | Number of downvotes the epic has received | | `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic | | `dueDate` | Time | Due date of the epic |
...@@ -334,6 +338,15 @@ Counts of descendent epics. ...@@ -334,6 +338,15 @@ Counts of descendent epics.
| `openedEpics` | Int | Number of opened sub-epics | | `openedEpics` | Int | Number of opened sub-epics |
| `openedIssues` | Int | Number of opened epic issues | | `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 ## EpicIssue
Relationship between an epic and an issue Relationship between an epic and an issue
...@@ -347,7 +360,7 @@ Relationship between an epic and an issue ...@@ -347,7 +360,7 @@ Relationship between an epic and an issue
| `description` | String | Description of the issue | | `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue | | `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 | | `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `downvotes` | Int! | Number of downvotes the issue has received | | `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue | | `dueDate` | Time | Due date of the issue |
...@@ -417,7 +430,7 @@ Autogenerated return type of EpicTreeReorder ...@@ -417,7 +430,7 @@ Autogenerated return type of EpicTreeReorder
| `enabled` | Boolean! | Indicates whether Grafana integration is enabled | | `enabled` | Boolean! | Indicates whether Grafana integration is enabled |
| `grafanaUrl` | String! | Url for the Grafana host for the Grafana integration | | `grafanaUrl` | String! | Url for the Grafana host for the Grafana integration |
| `id` | ID! | Internal ID of 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 | | `updatedAt` | Time! | Timestamp of the issue's last activity |
## Group ## Group
...@@ -469,7 +482,7 @@ Autogenerated return type of EpicTreeReorder ...@@ -469,7 +482,7 @@ Autogenerated return type of EpicTreeReorder
| `description` | String | Description of the issue | | `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue | | `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 | | `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `downvotes` | Int! | Number of downvotes the issue has received | | `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue | | `dueDate` | Time | Due date of the issue |
...@@ -578,7 +591,7 @@ Autogenerated return type of MarkAsSpamSnippet ...@@ -578,7 +591,7 @@ Autogenerated return type of MarkAsSpamSnippet
| `id` | ID! | ID of the merge request | | `id` | ID! | ID of the merge request |
| `iid` | String! | Internal 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 | | `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) | | `mergeCommitSha` | String | SHA of the merge request commit (set once merged) |
| `mergeError` | String | Error message due to a merge error | | `mergeError` | String | Error message due to a merge error |
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring | | `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
......
...@@ -30,6 +30,7 @@ If the `custom` level is used, specific email events can be controlled. Availabl ...@@ -30,6 +30,7 @@ If the `custom` level is used, specific email events can be controlled. Availabl
- `reassign_merge_request` - `reassign_merge_request`
- `merge_merge_request` - `merge_merge_request`
- `failed_pipeline` - `failed_pipeline`
- `fixed_pipeline`
- `success_pipeline` - `success_pipeline`
- `new_epic` **(ULTIMATE)** - `new_epic` **(ULTIMATE)**
...@@ -83,6 +84,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab. ...@@ -83,6 +84,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `reassign_merge_request` | boolean | no | Enable/disable this notification | | `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_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 | | `failed_pipeline` | boolean | no | Enable/disable this notification |
| `fixed_pipeline` | boolean | no | Enable/disable this notification |
| `success_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)** | | `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. ...@@ -152,6 +154,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `reassign_merge_request` | boolean | no | Enable/disable this notification | | `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_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 | | `failed_pipeline` | boolean | no | Enable/disable this notification |
| `fixed_pipeline` | boolean | no | Enable/disable this notification |
| `success_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)** | | `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: ...@@ -178,6 +181,7 @@ Example responses:
"reassign_merge_request": false, "reassign_merge_request": false,
"merge_merge_request": false, "merge_merge_request": false,
"failed_pipeline": false, "failed_pipeline": false,
"fixed_pipeline": false,
"success_pipeline": false "success_pipeline": false
} }
} }
......
...@@ -50,12 +50,12 @@ For indexing Git repository data, GitLab uses an [indexer written in Go](https:/ ...@@ -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: 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 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 ### GitLab Omnibus
The Go indexer was included in Omnibus GitLab 11.8 as an optional replacement to a Since GitLab 11.8 the Go indexer is included in GitLab Omnibus.
Ruby-based indexer. [Since GitLab v12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481), The former Ruby-based indexer was removed in [GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481).
### From source ### From source
......
...@@ -216,7 +216,7 @@ If you're using a custom setup for License Compliance, you're required ...@@ -216,7 +216,7 @@ If you're using a custom setup for License Compliance, you're required
to update your CI config accordingly: to update your CI config accordingly:
1. Change the CI template to `License-Scanning.gitlab-ci.yml`. 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`). 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`: For example, the following `.gitlab-ci.yml`:
......
...@@ -178,7 +178,8 @@ In most of the below cases, the notification will be sent to: ...@@ -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 | | 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 | | 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 | | 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)** | | | New epic **(ULTIMATE)** | |
| Close epic **(ULTIMATE)** | | | Close epic **(ULTIMATE)** | |
| Reopen epic **(ULTIMATE)** | | | Reopen epic **(ULTIMATE)** | |
......
...@@ -18,7 +18,6 @@ module Gitlab ...@@ -18,7 +18,6 @@ module Gitlab
self.table_name = 'epics' self.table_name = 'epics'
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :group belongs_to :group
def self.user_mention_model def self.user_mention_model
......
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
def exec def exec
if creation? || deletion? if creation? || deletion?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_delete_branch] raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch]
end end
# TODO: https://gitlab.com/gitlab-org/gitlab/issues/205628 # TODO: https://gitlab.com/gitlab-org/gitlab/issues/205628
......
...@@ -34,8 +34,7 @@ module Gitlab ...@@ -34,8 +34,7 @@ module Gitlab
ignore_auto_reply!(mail) ignore_auto_reply!(mail)
mail_key = extract_mail_key(mail) handler = find_handler(mail)
handler = Handler.for(mail, mail_key)
raise UnknownIncomingEmail unless handler raise UnknownIncomingEmail unless handler
...@@ -46,6 +45,11 @@ module Gitlab ...@@ -46,6 +45,11 @@ module Gitlab
private private
def find_handler(mail)
mail_key = extract_mail_key(mail)
Handler.for(mail, mail_key)
end
def build_mail def build_mail
Mail::Message.new(@raw) Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError, rescue Encoding::UndefinedConversionError,
......
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
# TODO: Investigate if expanding actor/authentication types are needed. # TODO: Investigate if expanding actor/authentication types are needed.
# https://gitlab.com/gitlab-org/gitlab/issues/202190 # https://gitlab.com/gitlab-org/gitlab/issues/202190
if actor && !actor.is_a?(User) && !actor.instance_of?(Key) if actor && !actor.is_a?(User) && !actor.instance_of?(Key)
raise UnauthorizedError, ERROR_MESSAGES[:authentication_mechanism] raise ForbiddenError, ERROR_MESSAGES[:authentication_mechanism]
end end
unless Feature.enabled?(:version_snippets, user) unless Feature.enabled?(:version_snippets, user)
...@@ -53,7 +53,7 @@ module Gitlab ...@@ -53,7 +53,7 @@ module Gitlab
override :check_push_access! override :check_push_access!
def 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! check_change_access!
end end
...@@ -74,7 +74,7 @@ module Gitlab ...@@ -74,7 +74,7 @@ module Gitlab
passed = guest_can_download_code? || user_can_download_code? passed = guest_can_download_code? || user_can_download_code?
unless passed unless passed
raise UnauthorizedError, ERROR_MESSAGES[:read_snippet] raise ForbiddenError, ERROR_MESSAGES[:read_snippet]
end end
end end
...@@ -91,7 +91,7 @@ module Gitlab ...@@ -91,7 +91,7 @@ module Gitlab
override :check_change_access! override :check_change_access!
def check_change_access! def check_change_access!
unless user_access.can_do_action?(:update_snippet) unless user_access.can_do_action?(:update_snippet)
raise UnauthorizedError, ERROR_MESSAGES[:update_snippet] raise ForbiddenError, ERROR_MESSAGES[:update_snippet]
end end
changes_list.each do |change| changes_list.each do |change|
......
...@@ -25,6 +25,28 @@ module Gitlab ...@@ -25,6 +25,28 @@ module Gitlab
fields.sort_by { |field| field[:name] } fields.sort_by { |field| field[:name] }
end 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 # Some fields types are arrays of other types and are displayed
# on docs wrapped in square brackets, for example: [String!]. # on docs wrapped in square brackets, for example: [String!].
# This makes GitLab docs renderer thinks they are links so here # This makes GitLab docs renderer thinks they are links so here
......
...@@ -11,6 +11,9 @@ ...@@ -11,6 +11,9 @@
Each table below documents a GraphQL type. Types match loosely to models, but not all 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. 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| - objects.each do |type|
- unless type[:fields].empty? - unless type[:fields].empty?
...@@ -22,5 +25,5 @@ ...@@ -22,5 +25,5 @@
~ "| Name | Type | Description |" ~ "| Name | Type | Description |"
~ "| --- | ---- | ---------- |" ~ "| --- | ---- | ---------- |"
- sorted_fields(type[:fields]).each do |field| - sorted_fields(type[:fields]).each do |field|
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |" = render_field(field)
\ \
...@@ -28,8 +28,9 @@ module Gitlab ...@@ -28,8 +28,9 @@ module Gitlab
config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}") config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end end
def key_from_address(address) def key_from_address(address, wildcard_address: nil)
regex = address_regex wildcard_address ||= config.address
regex = address_regex(wildcard_address)
return unless regex return unless regex
match = address.match(regex) match = address.match(regex)
...@@ -55,8 +56,7 @@ module Gitlab ...@@ -55,8 +56,7 @@ module Gitlab
private private
def address_regex def address_regex(wildcard_address)
wildcard_address = config.address
return unless wildcard_address return unless wildcard_address
regex = Regexp.escape(wildcard_address) regex = Regexp.escape(wildcard_address)
......
...@@ -20,6 +20,7 @@ module Gitlab ...@@ -20,6 +20,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
chain.add Gitlab::SidekiqStatus::ServerMiddleware chain.add Gitlab::SidekiqStatus::ServerMiddleware
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
chain.add Gitlab::SidekiqMiddleware::DuplicateJobs::Server
end end
end end
...@@ -33,6 +34,7 @@ module Gitlab ...@@ -33,6 +34,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add Labkit::Middleware::Sidekiq::Client chain.add Labkit::Middleware::Sidekiq::Client
chain.add Gitlab::SidekiqMiddleware::AdminMode::Client chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
chain.add Gitlab::SidekiqMiddleware::DuplicateJobs::Client
end end
end end
end end
......
# 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
# 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
# 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
# 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
# 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
...@@ -196,6 +196,11 @@ msgid_plural "%d unstaged changes" ...@@ -196,6 +196,11 @@ msgid_plural "%d unstaged changes"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" 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 "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "" msgstr[0] ""
...@@ -3930,6 +3935,9 @@ msgstr "" ...@@ -3930,6 +3935,9 @@ msgstr ""
msgid "Cluster cache cleared." msgid "Cluster cache cleared."
msgstr "" msgstr ""
msgid "Cluster does not exist"
msgstr ""
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr "" msgstr ""
...@@ -6700,6 +6708,9 @@ msgstr "" ...@@ -6700,6 +6708,9 @@ msgstr ""
msgid "Difference between start date and now" msgid "Difference between start date and now"
msgstr "" msgstr ""
msgid "DiffsCompareBaseBranch|(HEAD)"
msgstr ""
msgid "DiffsCompareBaseBranch|(base)" msgid "DiffsCompareBaseBranch|(base)"
msgstr "" msgstr ""
...@@ -6823,12 +6834,20 @@ msgstr "" ...@@ -6823,12 +6834,20 @@ msgstr ""
msgid "Dismiss" msgid "Dismiss"
msgstr "" msgstr ""
msgid "Dismiss %d selected vulnerability as"
msgid_plural "Dismiss %d selected vulnerabilities as"
msgstr[0] ""
msgstr[1] ""
msgid "Dismiss DevOps Score introduction" msgid "Dismiss DevOps Score introduction"
msgstr "" msgstr ""
msgid "Dismiss Merge Request promotion" msgid "Dismiss Merge Request promotion"
msgstr "" msgstr ""
msgid "Dismiss Selected"
msgstr ""
msgid "Dismiss Value Stream Analytics introduction box" msgid "Dismiss Value Stream Analytics introduction box"
msgstr "" msgstr ""
...@@ -7396,6 +7415,9 @@ msgstr "" ...@@ -7396,6 +7415,9 @@ msgstr ""
msgid "Environment" msgid "Environment"
msgstr "" 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." 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 "" msgstr ""
...@@ -8293,6 +8315,9 @@ msgstr "" ...@@ -8293,6 +8315,9 @@ msgstr ""
msgid "Failure" msgid "Failure"
msgstr "" 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." 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 "" msgstr ""
...@@ -12640,6 +12665,9 @@ msgstr "" ...@@ -12640,6 +12665,9 @@ msgstr ""
msgid "Name:" msgid "Name:"
msgstr "" msgstr ""
msgid "Namespace is empty"
msgstr ""
msgid "Namespace: %{namespace}" msgid "Namespace: %{namespace}"
msgstr "" msgstr ""
...@@ -12897,9 +12925,6 @@ msgstr "" ...@@ -12897,9 +12925,6 @@ msgstr ""
msgid "No data to display" msgid "No data to display"
msgstr "" msgstr ""
msgid "No deployment platform available"
msgstr ""
msgid "No deployments found" msgid "No deployments found"
msgstr "" msgstr ""
...@@ -13149,6 +13174,9 @@ msgstr "" ...@@ -13149,6 +13174,9 @@ msgstr ""
msgid "NotificationEvent|Failed pipeline" msgid "NotificationEvent|Failed pipeline"
msgstr "" msgstr ""
msgid "NotificationEvent|Fixed pipeline"
msgstr ""
msgid "NotificationEvent|Merge merge request" msgid "NotificationEvent|Merge merge request"
msgstr "" msgstr ""
...@@ -17056,6 +17084,9 @@ msgstr "" ...@@ -17056,6 +17084,9 @@ msgstr ""
msgid "Security Reports|There was an error deleting the comment." msgid "Security Reports|There was an error deleting the comment."
msgstr "" msgstr ""
msgid "Security Reports|There was an error dismissing the vulnerabilities."
msgstr ""
msgid "Security Reports|There was an error dismissing the vulnerability." msgid "Security Reports|There was an error dismissing the vulnerability."
msgstr "" msgstr ""
...@@ -17212,6 +17243,9 @@ msgstr "" ...@@ -17212,6 +17243,9 @@ msgstr ""
msgid "Select a project to read Insights configuration file" msgid "Select a project to read Insights configuration file"
msgstr "" msgstr ""
msgid "Select a reason"
msgstr ""
msgid "Select a repository" msgid "Select a repository"
msgstr "" msgstr ""
...@@ -22173,6 +22207,9 @@ msgstr "" ...@@ -22173,6 +22207,9 @@ msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
msgid "Won't fix / Accept risk"
msgstr ""
msgid "Work in progress Limit" msgid "Work in progress Limit"
msgstr "" msgstr ""
...@@ -22761,6 +22798,9 @@ msgstr "" ...@@ -22761,6 +22798,9 @@ msgstr ""
msgid "Zoom meeting removed" msgid "Zoom meeting removed"
msgstr "" msgstr ""
msgid "[No reason]"
msgstr ""
msgid "a deleted user" msgid "a deleted user"
msgstr "" msgstr ""
......
...@@ -14,4 +14,5 @@ N_('NotificationEvent|Close merge request') ...@@ -14,4 +14,5 @@ N_('NotificationEvent|Close merge request')
N_('NotificationEvent|Reassign merge request') N_('NotificationEvent|Reassign merge request')
N_('NotificationEvent|Merge merge request') N_('NotificationEvent|Merge merge request')
N_('NotificationEvent|Failed pipeline') N_('NotificationEvent|Failed pipeline')
N_('NotificationEvent|Fixed pipeline')
N_('NotificationEvent|New release') N_('NotificationEvent|New release')
# 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
...@@ -356,7 +356,7 @@ describe 'Pipeline', :js do ...@@ -356,7 +356,7 @@ describe 'Pipeline', :js do
end end
end end
context 'test tabs' do describe 'test tabs' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
before do before do
...@@ -364,21 +364,31 @@ describe 'Pipeline', :js do ...@@ -364,21 +364,31 @@ describe 'Pipeline', :js do
wait_for_requests wait_for_requests
end end
it 'shows badge counter in Tests tab' do context 'with test reports' do
expect(pipeline.test_reports.total_count).to eq(4) it 'shows badge counter in Tests tab' do
expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s) expect(pipeline.test_reports.total_count).to eq(4)
end 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 it 'does call test_report.json endpoint when tab is selected', :js do
expect(page).to have_selector('.js-no-tests-to-show', visible: :all) 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 end
it 'does call test_report.json endpoint when tab is selected', :js do context 'without test reports' do
find('.js-tests-tab-link').click let(:pipeline) { create(:ci_pipeline, project: project) }
wait_for_requests
expect(page).to have_content('Test suites') it 'shows nothing' do
expect(page).to have_selector('.js-tests-detail', visible: :all) expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("")
end
end end
end end
......
...@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue'; import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue';
import diffsMockData from '../mock_data/merge_request_diffs'; import diffsMockData from '../mock_data/merge_request_diffs';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { TEST_HOST } from 'helpers/test_constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
...@@ -109,6 +110,24 @@ describe('CompareVersionsDropdown', () => { ...@@ -109,6 +110,24 @@ describe('CompareVersionsDropdown', () => {
expect(findLastLink().attributes('href')).toEqual(baseVersionPath); expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
expect(findLastLink().text()).toContain('(base)'); 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', () => { it('should not render commits count if no showCommitsCount is passed', () => {
......
...@@ -21,6 +21,7 @@ describe NotificationsHelper do ...@@ -21,6 +21,7 @@ describe NotificationsHelper do
describe '#notification_event_name' do describe '#notification_event_name' do
it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') } 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(:failed_pipeline)).to match('Failed pipeline') }
it { expect(notification_event_name(:fixed_pipeline)).to match('Fixed pipeline') }
end end
describe '#notification_icon_level' do describe '#notification_icon_level' do
......
...@@ -19,7 +19,7 @@ describe Gitlab::Checks::SnippetCheck do ...@@ -19,7 +19,7 @@ describe Gitlab::Checks::SnippetCheck do
let(:newrev) { '0000000000000000000000000000000000000000' } let(:newrev) { '0000000000000000000000000000000000000000' }
it 'raises an error' do 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 end
...@@ -27,7 +27,7 @@ describe Gitlab::Checks::SnippetCheck do ...@@ -27,7 +27,7 @@ describe Gitlab::Checks::SnippetCheck do
let(:oldrev) { '0000000000000000000000000000000000000000' } let(:oldrev) { '0000000000000000000000000000000000000000' }
it 'raises an error' do 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 end
end end
......
...@@ -26,7 +26,7 @@ describe Gitlab::GitAccessSnippet do ...@@ -26,7 +26,7 @@ describe Gitlab::GitAccessSnippet do
let(:actor) { build(:deploy_key) } let(:actor) { build(:deploy_key) }
it 'does not allow push and pull access' do 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
end end
...@@ -76,8 +76,8 @@ describe Gitlab::GitAccessSnippet do ...@@ -76,8 +76,8 @@ describe Gitlab::GitAccessSnippet do
it 'blocks access when the user did not accept terms' 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/ message = /must accept the Terms of Service in order to perform this action/
expect { push_access_check }.to raise_unauthorized(message) expect { push_access_check }.to raise_forbidden(message)
expect { pull_access_check }.to raise_unauthorized(message) expect { pull_access_check }.to raise_forbidden(message)
end end
it 'allows access when the user accepted the terms' do it 'allows access when the user accepted the terms' do
...@@ -101,13 +101,13 @@ describe Gitlab::GitAccessSnippet do ...@@ -101,13 +101,13 @@ describe Gitlab::GitAccessSnippet do
if Ability.allowed?(user, :update_snippet, snippet) if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error expect { push_access_check }.not_to raise_error
else else
expect { push_access_check }.to raise_error(described_class::UnauthorizedError) expect { push_access_check }.to raise_error(described_class::ForbiddenError)
end end
if Ability.allowed?(user, :read_snippet, snippet) if Ability.allowed?(user, :read_snippet, snippet)
expect { pull_access_check }.not_to raise_error expect { pull_access_check }.not_to raise_error
else else
expect { pull_access_check }.to raise_error(described_class::UnauthorizedError) expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end end
end end
end end
...@@ -154,7 +154,7 @@ describe Gitlab::GitAccessSnippet do ...@@ -154,7 +154,7 @@ describe Gitlab::GitAccessSnippet do
with_them do with_them do
it "respects accessibility" do it "respects accessibility" do
error_class = described_class::UnauthorizedError error_class = described_class::ForbiddenError
if Ability.allowed?(user, :update_snippet, snippet) if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error expect { push_access_check }.not_to raise_error
...@@ -180,7 +180,7 @@ describe Gitlab::GitAccessSnippet do ...@@ -180,7 +180,7 @@ describe Gitlab::GitAccessSnippet do
allow(::Gitlab::Database).to receive(:read_only?).and_return(true) allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
allow(::Gitlab::Geo).to receive(:secondary_with_primary?).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
end end
...@@ -198,10 +198,10 @@ describe Gitlab::GitAccessSnippet do ...@@ -198,10 +198,10 @@ describe Gitlab::GitAccessSnippet do
it 'raises error if SnippetCheck raises error' do it 'raises error if SnippetCheck raises error' do
expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check| 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 end
expect { push_access_check }.to raise_unauthorized('foo') expect { push_access_check }.to raise_forbidden('foo')
end end
end end
...@@ -215,7 +215,7 @@ describe Gitlab::GitAccessSnippet do ...@@ -215,7 +215,7 @@ describe Gitlab::GitAccessSnippet do
raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found]) raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
end end
def raise_unauthorized(message) def raise_forbidden(message)
raise_error(Gitlab::GitAccess::UnauthorizedError, message) raise_error(Gitlab::GitAccess::ForbiddenError, message)
end end
end end
# 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
...@@ -192,6 +192,7 @@ ci_pipelines: ...@@ -192,6 +192,7 @@ ci_pipelines:
- environments - environments
- chat_data - chat_data
- source_pipeline - source_pipeline
- ref_status
- source_bridge - source_bridge
- source_job - source_job
- sourced_pipelines - sourced_pipelines
...@@ -359,6 +360,7 @@ project: ...@@ -359,6 +360,7 @@ project:
- ci_pipelines - ci_pipelines
- all_pipelines - all_pipelines
- stages - stages
- ci_refs
- builds - builds
- runner_projects - runner_projects
- runners - runners
......
...@@ -89,6 +89,17 @@ describe Gitlab::IncomingEmail do ...@@ -89,6 +89,17 @@ describe Gitlab::IncomingEmail do
it 'does not match emails with extra bits' 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 expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
end 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 end
context 'self.key_from_fallback_message_id' do context 'self.key_from_fallback_message_id' do
......
# 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
# 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
# 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
# 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
# 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
...@@ -46,7 +46,8 @@ describe Gitlab::SidekiqMiddleware do ...@@ -46,7 +46,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::MemoryKiller, Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware, Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
Gitlab::SidekiqMiddleware::WorkerContext::Server, Gitlab::SidekiqMiddleware::WorkerContext::Server,
Gitlab::SidekiqMiddleware::AdminMode::Server Gitlab::SidekiqMiddleware::AdminMode::Server,
Gitlab::SidekiqMiddleware::DuplicateJobs::Server
] ]
end end
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares } let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
...@@ -117,7 +118,8 @@ describe Gitlab::SidekiqMiddleware do ...@@ -117,7 +118,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::ClientMetrics, Gitlab::SidekiqMiddleware::ClientMetrics,
Gitlab::SidekiqMiddleware::WorkerContext::Client, Gitlab::SidekiqMiddleware::WorkerContext::Client,
Labkit::Middleware::Sidekiq::Client, Labkit::Middleware::Sidekiq::Client,
Gitlab::SidekiqMiddleware::AdminMode::Client Gitlab::SidekiqMiddleware::AdminMode::Client,
Gitlab::SidekiqMiddleware::DuplicateJobs::Client
] ]
end end
......
...@@ -106,4 +106,17 @@ describe Emails::Pipelines do ...@@ -106,4 +106,17 @@ describe Emails::Pipelines do
let(:status_text) { 'Your pipeline has failed.' } let(:status_text) { 'Your pipeline has failed.' }
end end
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 end
# 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
...@@ -176,8 +176,20 @@ describe NotificationRecipient do ...@@ -176,8 +176,20 @@ describe NotificationRecipient do
) )
end end
before do it 'returns true' do
notification_setting.update!(failed_pipeline: true) 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 end
it 'returns true' do it 'returns true' do
...@@ -185,7 +197,7 @@ describe NotificationRecipient do ...@@ -185,7 +197,7 @@ describe NotificationRecipient do
end end
end end
context "when action is not failed_pipeline" do context "when action is not fixed_pipeline or failed_pipeline" do
let(:recipient) do let(:recipient) do
described_class.new( described_class.new(
user, user,
...@@ -196,10 +208,6 @@ describe NotificationRecipient do ...@@ -196,10 +208,6 @@ describe NotificationRecipient do
) )
end end
before do
notification_setting.update!(success_pipeline: true)
end
it 'returns false' do it 'returns false' do
expect(recipient.suitable_notification_level?).to eq false expect(recipient.suitable_notification_level?).to eq false
end end
...@@ -309,6 +317,26 @@ describe NotificationRecipient do ...@@ -309,6 +317,26 @@ describe NotificationRecipient do
expect(recipient.suitable_notification_level?).to eq false expect(recipient.suitable_notification_level?).to eq false
end end
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
end end
......
...@@ -110,7 +110,8 @@ RSpec.describe NotificationSetting do ...@@ -110,7 +110,8 @@ RSpec.describe NotificationSetting do
:reassign_merge_request, :reassign_merge_request,
:merge_merge_request, :merge_merge_request,
:failed_pipeline, :failed_pipeline,
:success_pipeline :success_pipeline,
:fixed_pipeline
) )
end end
......
...@@ -72,6 +72,7 @@ describe Project do ...@@ -72,6 +72,7 @@ describe Project do
it { is_expected.to have_one(:project_setting) } it { is_expected.to have_one(:project_setting) }
it { is_expected.to have_many(:commit_statuses) } it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:ci_pipelines) } 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(:builds) }
it { is_expected.to have_many(:build_trace_section_names)} it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runner_projects) }
......
# 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
...@@ -2315,6 +2315,7 @@ describe NotificationService, :mailer do ...@@ -2315,6 +2315,7 @@ describe NotificationService, :mailer do
user = create_user_with_notification(:custom, 'custom_enabled') user = create_user_with_notification(:custom, 'custom_enabled')
update_custom_notification(:success_pipeline, user, resource: project) update_custom_notification(:success_pipeline, user, resource: project)
update_custom_notification(:failed_pipeline, user, resource: project) update_custom_notification(:failed_pipeline, user, resource: project)
update_custom_notification(:fixed_pipeline, user, resource: project)
user user
end end
...@@ -2322,6 +2323,7 @@ describe NotificationService, :mailer do ...@@ -2322,6 +2323,7 @@ describe NotificationService, :mailer do
user = create_user_with_notification(:custom, 'custom_disabled') user = create_user_with_notification(:custom, 'custom_disabled')
update_custom_notification(:success_pipeline, user, resource: project, value: false) update_custom_notification(:success_pipeline, user, resource: project, value: false)
update_custom_notification(:failed_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 user
end end
...@@ -2514,6 +2516,85 @@ describe NotificationService, :mailer do ...@@ -2514,6 +2516,85 @@ describe NotificationService, :mailer do
end end
end 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
end end
......
...@@ -65,9 +65,16 @@ shared_examples 'resource notes mentions migration' do |migration_class, resourc ...@@ -65,9 +65,16 @@ shared_examples 'resource notes mentions migration' do |migration_class, resourc
end end
shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| 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 it 'schedules background migrations' do
Sidekiq::Testing.fake! do Sidekiq::Testing.fake! do
Timecop.freeze do Timecop.freeze do
resource_count = is_for_notes ? Note.count : resource_class.count
expect(resource_count).to eq 5
migrate! migrate!
migration = described_class::MIGRATION migration = described_class::MIGRATION
......
# 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
...@@ -3,72 +3,8 @@ ...@@ -3,72 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe 'notify/pipeline_failed_email.html.haml' do describe 'notify/pipeline_failed_email.html.haml' do
include Devise::Test::ControllerHelpers it_behaves_like 'pipeline status changes email' do
let(:title) { 'Your pipeline has failed' }
let(:user) { create(:user, developer_projects: [project]) } let(:status) { :failed }
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'
end end
end end
# 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
# 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
...@@ -3,56 +3,8 @@ ...@@ -3,56 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe 'notify/pipeline_success_email.html.haml' do describe 'notify/pipeline_success_email.html.haml' do
include Devise::Test::ControllerHelpers it_behaves_like 'pipeline status changes email' do
let(:title) { 'Your pipeline has passed' }
let(:user) { create(:user, developer_projects: [project]) } let(:status) { :success }
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
end end
end end
...@@ -3,24 +3,8 @@ ...@@ -3,24 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe 'notify/pipeline_success_email.text.erb' do describe 'notify/pipeline_success_email.text.erb' do
let(:user) { create(:user, developer_projects: [project]) } it_behaves_like 'pipeline status changes email' do
let(:project) { create(:project, :repository) } let(:title) { 'Your pipeline has passed' }
let(:merge_request) { create(:merge_request, :simple, source_project: project) } let(:status) { :success }
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)
end end
it_behaves_like 'correct pipeline information for pipelines for merge requests'
end end
...@@ -3,13 +3,16 @@ ...@@ -3,13 +3,16 @@
require 'spec_helper' require 'spec_helper'
describe PipelineNotificationWorker, :mailer do describe PipelineNotificationWorker, :mailer do
let(:pipeline) { create(:ci_pipeline) } let_it_be(:pipeline) { create(:ci_pipeline) }
describe '#execute' do describe '#execute' do
it 'calls NotificationService#pipeline_finished when the pipeline exists' 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 end
it 'does nothing when the pipeline does not exist' do it 'does nothing when the pipeline does not exist' do
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment