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:
- 'app/views/notify/_failed_builds.html.haml'
- 'app/views/notify/_reassigned_issuable_email.html.haml'
- 'app/views/notify/_removal_notification.html.haml'
- 'app/views/notify/_successful_pipeline.html.haml'
- 'app/views/notify/autodevops_disabled_email.html.haml'
- 'app/views/notify/changed_milestone_email.html.haml'
- 'app/views/notify/import_issues_csv_email.html.haml'
......@@ -163,7 +164,6 @@ linters:
- 'app/views/notify/pages_domain_verification_failed_email.html.haml'
- 'app/views/notify/pages_domain_verification_succeeded_email.html.haml'
- 'app/views/notify/pipeline_failed_email.html.haml'
- 'app/views/notify/pipeline_success_email.html.haml'
- 'app/views/notify/project_was_exported_email.html.haml'
- 'app/views/notify/project_was_moved_email.html.haml'
- 'app/views/notify/project_was_not_exported_email.html.haml'
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { n__, __, sprintf } from '~/locale';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
......@@ -94,6 +95,9 @@ export default {
}
return version.versionIndex === -1;
},
isHead() {
return parseBoolean(getParameterByName('diff_head'));
},
isLatest(version) {
return (
this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index
......@@ -121,7 +125,8 @@ export default {
<div>
<strong>
{{ versionName(version) }}
<template v-if="isBase(version)">{{
<template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template>
<template v-else-if="isBase(version)">{{
s__('DiffsCompareBaseBranch|(base)')
}}</template>
</strong>
......
......@@ -134,6 +134,10 @@ export default () => {
axios
.get(dataset.testReportsCountEndpoint)
.then(({ data }) => {
if (!data.total_count) {
return;
}
document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
})
.catch(() => {});
......
<script>
import _ from 'underscore';
import { escape as esc } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
......@@ -43,10 +43,10 @@ You are going to change the username %{currentUsernameBold} to %{newUsernameBold
Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
Please update your Git repository remotes as soon as possible.`),
{
currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`,
newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`,
currentUsername: _.escape(this.username),
newUsername: _.escape(this.newUsername),
currentUsernameBold: `<strong>${esc(this.username)}</strong>`,
newUsernameBold: `<strong>${esc(this.newUsername)}</strong>`,
currentUsername: esc(this.username),
newUsername: esc(this.newUsername),
},
false,
);
......
......@@ -2,7 +2,7 @@
import $ from 'jquery';
import 'cropper';
import _ from 'underscore';
import { isString } from 'lodash';
(() => {
// Matches everything but the file name
......@@ -29,7 +29,7 @@ import _ from 'underscore';
this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input);
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.modalCropImg = isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput
.attr('name', `${this.fileInput.attr('name')}-trigger`)
.attr('id', `${this.fileInput.attr('id')}-trigger`);
......@@ -47,9 +47,9 @@ import _ from 'underscore';
this.filename = this.getElement(filename);
this.previewImage = this.getElement(previewImage);
this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.modalCrop = isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
this.modalCropImg = isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents();
}
......
......@@ -65,6 +65,7 @@
// Classes using mixins coming from @gitlab-ui
// can be removed once https://gitlab.com/gitlab-org/gitlab/merge_requests/19021 has been merged
.gl-bg-blue-50 { @include gl-bg-blue-50; }
.gl-bg-red-100 { @include gl-bg-red-100; }
.gl-bg-orange-100 { @include gl-bg-orange-100; }
.gl-bg-gray-100 { @include gl-bg-gray-100; }
......
......@@ -10,6 +10,10 @@ module Emails
pipeline_mail(pipeline, recipients, 'failed')
end
def pipeline_fixed_email(pipeline, recipients)
pipeline_mail(pipeline, recipients, 'been fixed')
end
private
def pipeline_mail(pipeline, recipients, status)
......
......@@ -145,6 +145,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
end
def pipeline_fixed_email
Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email))
end
def autodevops_disabled_email
Notify.autodevops_disabled_email(pipeline, user.email).message
end
......
......@@ -63,6 +63,14 @@ module Ci
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id
has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline
has_one :ref_status, ->(pipeline) {
# We use .read_attribute to save 1 extra unneeded query to load the :project.
unscope(:where)
.where(project_id: pipeline.read_attribute(:project_id), ref: pipeline.ref, tag: pipeline.tag)
# Sadly :inverse_of is not supported (yet) by Rails for composite PKs.
}, class_name: 'Ci::Ref', inverse_of: :pipelines
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
......@@ -227,7 +235,7 @@ module Ci
after_transition any => [:success, :failed] do |pipeline|
pipeline.run_after_commit do
PipelineNotificationWorker.perform_async(pipeline.id)
PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id)
end
end
......
# 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
when :mention
@type == :mention
when :participating
@custom_action == :failed_pipeline || %i[participating mention].include?(@type)
%i[failed_pipeline fixed_pipeline].include?(@custom_action) ||
%i[participating mention].include?(@type)
when :custom
custom_enabled? || %i[participating mention].include?(@type)
when :watch
......@@ -63,7 +64,13 @@ class NotificationRecipient
end
def custom_enabled?
@custom_action && notification_setting&.event_enabled?(@custom_action)
return false unless @custom_action
return false unless notification_setting
notification_setting.event_enabled?(@custom_action) ||
# fixed_pipeline is a subset of success_pipeline event
(@custom_action == :fixed_pipeline &&
notification_setting.event_enabled?(:success_pipeline))
end
def unsubscribed?
......
......@@ -44,6 +44,7 @@ class NotificationSetting < ApplicationRecord
:reassign_merge_request,
:merge_merge_request,
:failed_pipeline,
:fixed_pipeline,
:success_pipeline
].freeze
......@@ -76,9 +77,9 @@ class NotificationSetting < ApplicationRecord
setting
end
# Allow people to receive failed pipeline notifications if they already have
# custom notifications enabled, as these are more like mentions than the other
# custom settings.
# Allow people to receive both failed pipeline/fixed pipeline notifications
# if they already have custom notifications enabled,
# as these are more like mentions than the other custom settings.
def failed_pipeline
bool = super
......@@ -86,6 +87,13 @@ class NotificationSetting < ApplicationRecord
end
alias_method :failed_pipeline?, :failed_pipeline
def fixed_pipeline
bool = super
bool.nil? || bool
end
alias_method :fixed_pipeline?, :fixed_pipeline
def event_enabled?(event)
respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend
end
......
......@@ -267,6 +267,7 @@ class Project < ApplicationRecord
class_name: 'Ci::Pipeline',
inverse_of: :project
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
has_many :ci_refs, class_name: 'Ci::Ref'
# Ci::Build objects store data on the file system such as artifact files and
# build traces. Currently there's no efficient way of removing this data in
......
......@@ -49,7 +49,7 @@ class PipelinesEmailService < Service
return unless all_recipients.any?
pipeline_id = data[:object_attributes][:id]
PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients)
end
def can_test?
......
# 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
end
def create_detached_merge_request_pipeline(merge_request)
if can_use_merge_request_ref?(merge_request)
Ci::CreatePipelineService.new(merge_request.source_project, current_user,
ref: merge_request.ref_path)
.execute(:merge_request_event, merge_request: merge_request)
else
Ci::CreatePipelineService.new(merge_request.source_project, current_user,
ref: merge_request.source_branch)
.execute(:merge_request_event, merge_request: merge_request)
end
Ci::CreatePipelineService.new(merge_request.source_project,
current_user,
ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request))
.execute(:merge_request_event, merge_request: merge_request)
end
def can_create_pipeline_for?(merge_request)
......@@ -33,6 +28,16 @@ module MergeRequests
def allow_duplicate
params[:allow_duplicate]
end
private
def pipeline_ref_for_detached_merge_request_pipeline(merge_request)
if can_use_merge_request_ref?(merge_request)
merge_request.ref_path
else
merge_request.source_branch
end
end
end
end
......
......@@ -434,18 +434,19 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
def pipeline_finished(pipeline, recipients = nil)
def pipeline_finished(pipeline, ref_status: nil, recipients: nil)
# Must always check project configuration since recipients could be a list of emails
# from the PipelinesEmailService integration.
return if pipeline.project.emails_disabled?
email_template = "pipeline_#{pipeline.status}_email"
ref_status ||= pipeline.status
email_template = "pipeline_#{ref_status}_email"
return unless mailer.respond_to?(email_template)
recipients ||= notifiable_users(
[pipeline.user], :watch,
custom_action: :"#{pipeline.status}_pipeline",
custom_action: :"#{ref_status}_pipeline",
target: pipeline
).map do |user|
user.notification_email_for(pipeline.project.group)
......
- 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!' -%>
Your pipeline has passed.
Project: <%= @project.name %> ( <%= project_url(@project) %> )
Branch: <%= @pipeline.source_ref %> ( <%= commits_url(@pipeline) %> )
<% if @merge_request -%>
Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> )
<% end -%>
Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> )
Commit Message: <%= @pipeline.git_commit_message.truncate(50) %>
<% commit = @pipeline.commit -%>
<% if commit.author -%>
Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
<% if commit.different_committer? -%>
<% if commit.committer -%>
Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> )
<% else -%>
Committed by: <%= commit.committer_name %>
<% end -%>
<% end -%>
<% job_count = @pipeline.total_size -%>
<% stage_count = @pipeline.stages_count -%>
<% if @pipeline.user -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> )
<% else -%>
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
<%= render 'notify/successful_pipeline', title: 'Your pipeline has passed.' -%>
......@@ -668,6 +668,13 @@
:resource_boundary: :cpu
:weight: 3
:idempotent:
- :name: pipeline_default:pipeline_update_ci_ref_status
:feature_category: :continuous_integration
:has_external_dependencies:
:latency_sensitive: true
:resource_boundary: :cpu
:weight: 3
:idempotent:
- :name: pipeline_hooks:build_hooks
:feature_category: :continuous_integration
:has_external_dependencies:
......
......@@ -8,12 +8,20 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id, recipients = nil)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
def perform(pipeline_id, args = {})
case args
when Hash
ref_status = args[:ref_status]
recipients = args[:recipients]
else # TODO: backward compatible interface, can be removed in 12.10
recipients = args
ref_status = nil
end
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
NotificationService.new.pipeline_finished(pipeline, recipients)
NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients)
end
# rubocop: enable CodeReuse/ActiveRecord
end
# 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
Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
#
# Service desk email
#
Settings['service_desk_email'] ||= Settingslogic.new({})
Settings.service_desk_email['enabled'] = false if Settings.service_desk_email['enabled'].nil?
#
# Build Artifacts
#
......
# 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
class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
......@@ -26,7 +28,7 @@ class MigrateEpicMentionsToDb < ActiveRecord::Migration[5.2]
.where(QUERY_CONDITIONS)
.each_batch(of: BATCH_SIZE) do |batch, index|
range = batch.pluck(Arel.sql('MIN(epics.id)'), Arel.sql('MAX(epics.id)')).first
BackgroundMigrationWorker.perform_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
migrate_in(index * DELAY, MIGRATION, ['Epic', JOIN, QUERY_CONDITIONS, false, *range])
end
end
......
# 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 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_02_24_163804) do
ActiveRecord::Schema.define(version: 2020_02_26_162723) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
......@@ -878,6 +878,17 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.index ["pipeline_id"], name: "index_ci_pipelines_config_on_pipeline_id"
end
create_table "ci_refs", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "lock_version", default: 0
t.integer "last_updated_by_pipeline_id"
t.boolean "tag", default: false, null: false
t.string "ref", limit: 255, null: false
t.string "status", limit: 255, null: false
t.index ["last_updated_by_pipeline_id"], name: "index_ci_refs_on_last_updated_by_pipeline_id"
t.index ["project_id", "ref", "tag"], name: "index_ci_refs_on_project_id_and_ref_and_tag", unique: true
end
create_table "ci_resource_groups", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
......@@ -2845,6 +2856,7 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.boolean "issue_due"
t.boolean "new_epic"
t.string "notification_email"
t.boolean "fixed_pipeline"
t.boolean "new_release"
t.index ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type"
t.index ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true
......@@ -4356,8 +4368,6 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.text "description_html"
t.bigint "start_date_sourcing_milestone_id"
t.bigint "due_date_sourcing_milestone_id"
t.bigint "closed_by_id"
t.datetime_with_timezone "closed_at"
t.integer "state", limit: 2, default: 1, null: false
t.integer "severity", limit: 2, null: false
t.boolean "severity_overridden", default: false
......@@ -4369,9 +4379,11 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
t.integer "cached_markdown_version"
t.bigint "confirmed_by_id"
t.datetime_with_timezone "confirmed_at"
t.datetime_with_timezone "dismissed_at"
t.bigint "dismissed_by_id"
t.index ["author_id"], name: "index_vulnerabilities_on_author_id"
t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id"
t.index ["confirmed_by_id"], name: "index_vulnerabilities_on_confirmed_by_id"
t.index ["dismissed_by_id"], name: "index_vulnerabilities_on_dismissed_by_id"
t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id"
t.index ["epic_id"], name: "index_vulnerabilities_on_epic_id"
t.index ["last_edited_by_id"], name: "index_vulnerabilities_on_last_edited_by_id"
......@@ -4650,6 +4662,8 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
add_foreign_key "ci_pipelines_config", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "ci_refs", "ci_pipelines", column: "last_updated_by_pipeline_id", on_delete: :nullify
add_foreign_key "ci_refs", "projects", on_delete: :cascade
add_foreign_key "ci_resource_groups", "projects", name: "fk_774722d144", on_delete: :cascade
add_foreign_key "ci_resources", "ci_builds", column: "build_id", name: "fk_e169a8e3d5", on_delete: :nullify
add_foreign_key "ci_resources", "ci_resource_groups", column: "resource_group_id", on_delete: :cascade
......@@ -5026,8 +5040,8 @@ ActiveRecord::Schema.define(version: 2020_02_24_163804) do
add_foreign_key "vulnerabilities", "milestones", name: "fk_131d289c65", on_delete: :nullify
add_foreign_key "vulnerabilities", "projects", name: "fk_efb96ab1e2", on_delete: :cascade
add_foreign_key "vulnerabilities", "users", column: "author_id", name: "fk_b1de915a15", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "closed_by_id", name: "fk_cf5c60acbf", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "confirmed_by_id", name: "fk_959d40ad0a", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "dismissed_by_id", name: "fk_725465b774", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "last_edited_by_id", name: "fk_1302949740", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "resolved_by_id", name: "fk_76bc5f5455", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "updated_by_id", name: "fk_7ac31eacb9", on_delete: :nullify
......
......@@ -1862,6 +1862,12 @@ type Epic implements Noteable {
"""
descendantCounts: EpicDescendantCount
"""
Total weight of open and closed descendant epic's issues. Available only when
feature flag unfiltered_epic_aggregates is enabled.
"""
descendantWeightSum: EpicDescendantWeights
"""
Description of the epic
"""
......@@ -2178,6 +2184,21 @@ type EpicDescendantCount {
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.
"""
......
......@@ -13,6 +13,9 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
Each table below documents a GraphQL type. Types match loosely to models, but not all
fields and methods on a model are available via GraphQL.
CAUTION: **Caution:**
Fields that are deprecated are marked with **{warning-solid}**.
## AddAwardEmojiPayload
Autogenerated return type of AddAwardEmoji
......@@ -69,7 +72,7 @@ Represents a project or group board
| `authoredDate` | Time | Timestamp of when the commit was authored |
| `description` | String | Description of the commit message |
| `id` | ID! | ID (global ID) of the commit |
| `latestPipeline` | Pipeline | Latest pipeline of the commit |
| `latestPipeline` **{warning-solid}** | Pipeline | **Deprecated:** Use pipelines |
| `message` | String | Raw commit message |
| `sha` | String! | SHA1 ID of the commit |
| `signatureHtml` | String | Rendered HTML of the commit signature |
......@@ -294,6 +297,7 @@ Represents an epic.
| `closedAt` | Time | Timestamp of the epic's closure |
| `createdAt` | Time | Timestamp of the epic's creation |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed descendant epic's issues. Available only when feature flag unfiltered_epic_aggregates is enabled. |
| `description` | String | Description of the epic |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
......@@ -334,6 +338,15 @@ Counts of descendent epics.
| `openedEpics` | Int | Number of opened sub-epics |
| `openedIssues` | Int | Number of opened epic issues |
## EpicDescendantWeights
Total weight of open and closed descendant issues
| Name | Type | Description |
| --- | ---- | ---------- |
| `closedIssues` | Int | Total weight of completed (closed) issues in this epic, including epic descendants |
| `openedIssues` | Int | Total weight of opened issues in this epic, including epic descendants |
## EpicIssue
Relationship between an epic and an issue
......@@ -347,7 +360,7 @@ Relationship between an epic and an issue
| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue |
| `designs` | DesignCollection | Deprecated. Use `designCollection` |
| `designs` **{warning-solid}** | DesignCollection | **Deprecated:** Use designCollection |
| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
......@@ -417,7 +430,7 @@ Autogenerated return type of EpicTreeReorder
| `enabled` | Boolean! | Indicates whether Grafana integration is enabled |
| `grafanaUrl` | String! | Url for the Grafana host for the Grafana integration |
| `id` | ID! | Internal ID of the Grafana integration |
| `token` | String! | API token for the Grafana integration. Field is permanently masked. |
| `token` **{warning-solid}** | String! | **Deprecated:** Plain text token has been masked for security reasons |
| `updatedAt` | Time! | Timestamp of the issue's last activity |
## Group
......@@ -469,7 +482,7 @@ Autogenerated return type of EpicTreeReorder
| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `designCollection` | DesignCollection | Collection of design images associated with this issue |
| `designs` | DesignCollection | Deprecated. Use `designCollection` |
| `designs` **{warning-solid}** | DesignCollection | **Deprecated:** Use designCollection |
| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
......@@ -578,7 +591,7 @@ Autogenerated return type of MarkAsSpamSnippet
| `id` | ID! | ID of the merge request |
| `iid` | String! | Internal ID of the merge request |
| `inProgressMergeCommitSha` | String | Commit SHA of the merge request if merge is in progress |
| `mergeCommitMessage` | String | Deprecated - renamed to defaultMergeCommitMessage |
| `mergeCommitMessage` **{warning-solid}** | String | **Deprecated:** Renamed to defaultMergeCommitMessage |
| `mergeCommitSha` | String | SHA of the merge request commit (set once merged) |
| `mergeError` | String | Error message due to a merge error |
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
......
......@@ -30,6 +30,7 @@ If the `custom` level is used, specific email events can be controlled. Availabl
- `reassign_merge_request`
- `merge_merge_request`
- `failed_pipeline`
- `fixed_pipeline`
- `success_pipeline`
- `new_epic` **(ULTIMATE)**
......@@ -83,6 +84,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
| `fixed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6626) in 11.3) **(ULTIMATE)** |
......@@ -152,6 +154,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
| `failed_pipeline` | boolean | no | Enable/disable this notification |
| `fixed_pipeline` | boolean | no | Enable/disable this notification |
| `success_pipeline` | boolean | no | Enable/disable this notification |
| `new_epic` | boolean | no | Enable/disable this notification ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6626) in 11.3) **(ULTIMATE)** |
......@@ -178,6 +181,7 @@ Example responses:
"reassign_merge_request": false,
"merge_merge_request": false,
"failed_pipeline": false,
"fixed_pipeline": false,
"success_pipeline": false
}
}
......
......@@ -50,12 +50,12 @@ For indexing Git repository data, GitLab uses an [indexer written in Go](https:/
The way you install the Go indexer depends on your version of GitLab:
- For GitLab Omnibus 11.8 and above, see [GitLab Omnibus](#gitlab-omnibus).
- For older versions of GitLab, install the indexer [From Source](#from-source).
- For installations from source or older versions of GitLab Omnibus, install the indexer [From Source](#from-source).
### GitLab Omnibus
The Go indexer was included in Omnibus GitLab 11.8 as an optional replacement to a
Ruby-based indexer. [Since GitLab v12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481),
Since GitLab 11.8 the Go indexer is included in GitLab Omnibus.
The former Ruby-based indexer was removed in [GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481).
### From source
......
......@@ -216,7 +216,7 @@ If you're using a custom setup for License Compliance, you're required
to update your CI config accordingly:
1. Change the CI template to `License-Scanning.gitlab-ci.yml`.
1. Change the job name to `license_management` (if you mention it in `.gitlab-ci.yml`).
1. Change the job name to `license_scanning` (if you mention it in `.gitlab-ci.yml`).
1. Change the artifact name to `gl-license-scanning-report.json` (if you mention it in `.gitlab-ci.yml`).
For example, the following `.gitlab-ci.yml`:
......
......@@ -178,7 +178,8 @@ In most of the below cases, the notification will be sent to:
| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
| Failed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
| Fixed pipeline | The author of the pipeline |
| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set. If the pipeline failed previously, a `Fixed pipeline` message will be sent for the first successful pipeline after the failure, then a `Successful pipeline` message for any further successful pipelines. |
| New epic **(ULTIMATE)** | |
| Close epic **(ULTIMATE)** | |
| Reopen epic **(ULTIMATE)** | |
......
......@@ -18,7 +18,6 @@ module Gitlab
self.table_name = 'epics'
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :group
def self.user_mention_model
......
......@@ -21,7 +21,7 @@ module Gitlab
def exec
if creation? || deletion?
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_delete_branch]
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:create_delete_branch]
end
# TODO: https://gitlab.com/gitlab-org/gitlab/issues/205628
......
......@@ -34,8 +34,7 @@ module Gitlab
ignore_auto_reply!(mail)
mail_key = extract_mail_key(mail)
handler = Handler.for(mail, mail_key)
handler = find_handler(mail)
raise UnknownIncomingEmail unless handler
......@@ -46,6 +45,11 @@ module Gitlab
private
def find_handler(mail)
mail_key = extract_mail_key(mail)
Handler.for(mail, mail_key)
end
def build_mail
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
......
......@@ -28,7 +28,7 @@ module Gitlab
# TODO: Investigate if expanding actor/authentication types are needed.
# https://gitlab.com/gitlab-org/gitlab/issues/202190
if actor && !actor.is_a?(User) && !actor.instance_of?(Key)
raise UnauthorizedError, ERROR_MESSAGES[:authentication_mechanism]
raise ForbiddenError, ERROR_MESSAGES[:authentication_mechanism]
end
unless Feature.enabled?(:version_snippets, user)
......@@ -53,7 +53,7 @@ module Gitlab
override :check_push_access!
def check_push_access!
raise UnauthorizedError, ERROR_MESSAGES[:update_snippet] unless user
raise ForbiddenError, ERROR_MESSAGES[:update_snippet] unless user
check_change_access!
end
......@@ -74,7 +74,7 @@ module Gitlab
passed = guest_can_download_code? || user_can_download_code?
unless passed
raise UnauthorizedError, ERROR_MESSAGES[:read_snippet]
raise ForbiddenError, ERROR_MESSAGES[:read_snippet]
end
end
......@@ -91,7 +91,7 @@ module Gitlab
override :check_change_access!
def check_change_access!
unless user_access.can_do_action?(:update_snippet)
raise UnauthorizedError, ERROR_MESSAGES[:update_snippet]
raise ForbiddenError, ERROR_MESSAGES[:update_snippet]
end
changes_list.each do |change|
......
......@@ -25,6 +25,28 @@ module Gitlab
fields.sort_by { |field| field[:name] }
end
def render_field(field)
'| %s | %s | %s |' % [
render_field_name(field),
render_field_type(field[:type][:info]),
render_field_description(field)
]
end
def render_field_name(field)
rendered_name = "`#{field[:name]}`"
rendered_name += ' **{warning-solid}**' if field[:is_deprecated]
rendered_name
end
# Returns the field description. If the field has been deprecated,
# the deprecation reason will be returned in place of the description.
def render_field_description(field)
return field[:description] unless field[:is_deprecated]
"**Deprecated:** #{field[:deprecation_reason]}"
end
# Some fields types are arrays of other types and are displayed
# on docs wrapped in square brackets, for example: [String!].
# This makes GitLab docs renderer thinks they are links so here
......
......@@ -11,6 +11,9 @@
Each table below documents a GraphQL type. Types match loosely to models, but not all
fields and methods on a model are available via GraphQL.
CAUTION: **Caution:**
Fields that are deprecated are marked with **{warning-solid}**.
\
- objects.each do |type|
- unless type[:fields].empty?
......@@ -22,5 +25,5 @@
~ "| Name | Type | Description |"
~ "| --- | ---- | ---------- |"
- sorted_fields(type[:fields]).each do |field|
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
= render_field(field)
\
......@@ -28,8 +28,9 @@ module Gitlab
config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end
def key_from_address(address)
regex = address_regex
def key_from_address(address, wildcard_address: nil)
wildcard_address ||= config.address
regex = address_regex(wildcard_address)
return unless regex
match = address.match(regex)
......@@ -55,8 +56,7 @@ module Gitlab
private
def address_regex
wildcard_address = config.address
def address_regex(wildcard_address)
return unless wildcard_address
regex = Regexp.escape(wildcard_address)
......
......@@ -20,6 +20,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
chain.add Gitlab::SidekiqStatus::ServerMiddleware
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
chain.add Gitlab::SidekiqMiddleware::DuplicateJobs::Server
end
end
......@@ -33,6 +34,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add Labkit::Middleware::Sidekiq::Client
chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
chain.add Gitlab::SidekiqMiddleware::DuplicateJobs::Client
end
end
end
......
# 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"
msgstr[0] ""
msgstr[1] ""
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
msgstr[1] ""
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
......@@ -3930,6 +3935,9 @@ msgstr ""
msgid "Cluster cache cleared."
msgstr ""
msgid "Cluster does not exist"
msgstr ""
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr ""
......@@ -6700,6 +6708,9 @@ msgstr ""
msgid "Difference between start date and now"
msgstr ""
msgid "DiffsCompareBaseBranch|(HEAD)"
msgstr ""
msgid "DiffsCompareBaseBranch|(base)"
msgstr ""
......@@ -6823,12 +6834,20 @@ msgstr ""
msgid "Dismiss"
msgstr ""
msgid "Dismiss %d selected vulnerability as"
msgid_plural "Dismiss %d selected vulnerabilities as"
msgstr[0] ""
msgstr[1] ""
msgid "Dismiss DevOps Score introduction"
msgstr ""
msgid "Dismiss Merge Request promotion"
msgstr ""
msgid "Dismiss Selected"
msgstr ""
msgid "Dismiss Value Stream Analytics introduction box"
msgstr ""
......@@ -7396,6 +7415,9 @@ msgstr ""
msgid "Environment"
msgstr ""
msgid "Environment does not have deployments"
msgstr ""
msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
msgstr ""
......@@ -8293,6 +8315,9 @@ msgstr ""
msgid "Failure"
msgstr ""
msgid "False positive"
msgstr ""
msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged."
msgstr ""
......@@ -12640,6 +12665,9 @@ msgstr ""
msgid "Name:"
msgstr ""
msgid "Namespace is empty"
msgstr ""
msgid "Namespace: %{namespace}"
msgstr ""
......@@ -12897,9 +12925,6 @@ msgstr ""
msgid "No data to display"
msgstr ""
msgid "No deployment platform available"
msgstr ""
msgid "No deployments found"
msgstr ""
......@@ -13149,6 +13174,9 @@ msgstr ""
msgid "NotificationEvent|Failed pipeline"
msgstr ""
msgid "NotificationEvent|Fixed pipeline"
msgstr ""
msgid "NotificationEvent|Merge merge request"
msgstr ""
......@@ -17056,6 +17084,9 @@ msgstr ""
msgid "Security Reports|There was an error deleting the comment."
msgstr ""
msgid "Security Reports|There was an error dismissing the vulnerabilities."
msgstr ""
msgid "Security Reports|There was an error dismissing the vulnerability."
msgstr ""
......@@ -17212,6 +17243,9 @@ msgstr ""
msgid "Select a project to read Insights configuration file"
msgstr ""
msgid "Select a reason"
msgstr ""
msgid "Select a repository"
msgstr ""
......@@ -22173,6 +22207,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
msgid "Won't fix / Accept risk"
msgstr ""
msgid "Work in progress Limit"
msgstr ""
......@@ -22761,6 +22798,9 @@ msgstr ""
msgid "Zoom meeting removed"
msgstr ""
msgid "[No reason]"
msgstr ""
msgid "a deleted user"
msgstr ""
......
......@@ -14,4 +14,5 @@ N_('NotificationEvent|Close merge request')
N_('NotificationEvent|Reassign merge request')
N_('NotificationEvent|Merge merge request')
N_('NotificationEvent|Failed pipeline')
N_('NotificationEvent|Fixed pipeline')
N_('NotificationEvent|New release')
# 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
end
end
context 'test tabs' do
describe 'test tabs' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
before do
......@@ -364,21 +364,31 @@ describe 'Pipeline', :js do
wait_for_requests
end
it 'shows badge counter in Tests tab' do
expect(pipeline.test_reports.total_count).to eq(4)
expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s)
end
context 'with test reports' do
it 'shows badge counter in Tests tab' do
expect(pipeline.test_reports.total_count).to eq(4)
expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s)
end
it 'does not call test_report.json endpoint by default', :js do
expect(page).to have_selector('.js-no-tests-to-show', visible: :all)
end
it 'does not call test_report.json endpoint by default', :js do
expect(page).to have_selector('.js-no-tests-to-show', visible: :all)
it 'does call test_report.json endpoint when tab is selected', :js do
find('.js-tests-tab-link').click
wait_for_requests
expect(page).to have_content('Test suites')
expect(page).to have_selector('.js-tests-detail', visible: :all)
end
end
it 'does call test_report.json endpoint when tab is selected', :js do
find('.js-tests-tab-link').click
wait_for_requests
context 'without test reports' do
let(:pipeline) { create(:ci_pipeline, project: project) }
expect(page).to have_content('Test suites')
expect(page).to have_selector('.js-tests-detail', visible: :all)
it 'shows nothing' do
expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("")
end
end
end
......
......@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue';
import diffsMockData from '../mock_data/merge_request_diffs';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { TEST_HOST } from 'helpers/test_constants';
const localVue = createLocalVue();
const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
......@@ -109,6 +110,24 @@ describe('CompareVersionsDropdown', () => {
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
expect(findLastLink().text()).toContain('(base)');
expect(findLastLink().text()).not.toContain('(HEAD)');
});
it('should render a correct head version link', () => {
Object.defineProperty(window, 'location', {
writable: true,
value: { href: `${TEST_HOST}?diff_head=true` },
});
createComponent({
baseVersionPath,
otherVersions: diffsMockData.slice(1),
targetBranch,
});
expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
expect(findLastLink().text()).not.toContain('(base)');
expect(findLastLink().text()).toContain('(HEAD)');
});
it('should not render commits count if no showCommitsCount is passed', () => {
......
......@@ -21,6 +21,7 @@ describe NotificationsHelper do
describe '#notification_event_name' do
it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') }
it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') }
it { expect(notification_event_name(:fixed_pipeline)).to match('Fixed pipeline') }
end
describe '#notification_icon_level' do
......
......@@ -19,7 +19,7 @@ describe Gitlab::Checks::SnippetCheck do
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'raises an error' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can not create or delete branches.')
expect { subject.exec }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
end
end
......@@ -27,7 +27,7 @@ describe Gitlab::Checks::SnippetCheck do
let(:oldrev) { '0000000000000000000000000000000000000000' }
it 'raises an error' do
expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can not create or delete branches.')
expect { subject.exec }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.')
end
end
end
......
......@@ -26,7 +26,7 @@ describe Gitlab::GitAccessSnippet do
let(:actor) { build(:deploy_key) }
it 'does not allow push and pull access' do
expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:authentication_mechanism])
expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism])
end
end
......@@ -76,8 +76,8 @@ describe Gitlab::GitAccessSnippet do
it 'blocks access when the user did not accept terms' do
message = /must accept the Terms of Service in order to perform this action/
expect { push_access_check }.to raise_unauthorized(message)
expect { pull_access_check }.to raise_unauthorized(message)
expect { push_access_check }.to raise_forbidden(message)
expect { pull_access_check }.to raise_forbidden(message)
end
it 'allows access when the user accepted the terms' do
......@@ -101,13 +101,13 @@ describe Gitlab::GitAccessSnippet do
if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error
else
expect { push_access_check }.to raise_error(described_class::UnauthorizedError)
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
end
if Ability.allowed?(user, :read_snippet, snippet)
expect { pull_access_check }.not_to raise_error
else
expect { pull_access_check }.to raise_error(described_class::UnauthorizedError)
expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
......@@ -154,7 +154,7 @@ describe Gitlab::GitAccessSnippet do
with_them do
it "respects accessibility" do
error_class = described_class::UnauthorizedError
error_class = described_class::ForbiddenError
if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error
......@@ -180,7 +180,7 @@ describe Gitlab::GitAccessSnippet do
allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true)
expect { push_access_check }.to raise_unauthorized(/You can't push code to a read-only GitLab instance/)
expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
end
end
......@@ -198,10 +198,10 @@ describe Gitlab::GitAccessSnippet do
it 'raises error if SnippetCheck raises error' do
expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
allow(check).to receive(:exec).and_raise(Gitlab::GitAccess::UnauthorizedError, 'foo')
allow(check).to receive(:exec).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo')
end
expect { push_access_check }.to raise_unauthorized('foo')
expect { push_access_check }.to raise_forbidden('foo')
end
end
......@@ -215,7 +215,7 @@ describe Gitlab::GitAccessSnippet do
raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
end
def raise_unauthorized(message)
raise_error(Gitlab::GitAccess::UnauthorizedError, message)
def raise_forbidden(message)
raise_error(Gitlab::GitAccess::ForbiddenError, message)
end
end
# 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:
- environments
- chat_data
- source_pipeline
- ref_status
- source_bridge
- source_job
- sourced_pipelines
......@@ -359,6 +360,7 @@ project:
- ci_pipelines
- all_pipelines
- stages
- ci_refs
- builds
- runner_projects
- runners
......
......@@ -89,6 +89,17 @@ describe Gitlab::IncomingEmail do
it 'does not match emails with extra bits' do
expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
end
context 'when a custom wildcard address is used' do
let(:wildcard_address) { 'custom.address+%{key}@example.com' }
it 'finds key if email matches address pattern' do
key = described_class.key_from_address(
'custom.address+foo@example.com', wildcard_address: wildcard_address
)
expect(key).to eq('foo')
end
end
end
context 'self.key_from_fallback_message_id' do
......
# 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
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
Gitlab::SidekiqMiddleware::WorkerContext::Server,
Gitlab::SidekiqMiddleware::AdminMode::Server
Gitlab::SidekiqMiddleware::AdminMode::Server,
Gitlab::SidekiqMiddleware::DuplicateJobs::Server
]
end
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
......@@ -117,7 +118,8 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::ClientMetrics,
Gitlab::SidekiqMiddleware::WorkerContext::Client,
Labkit::Middleware::Sidekiq::Client,
Gitlab::SidekiqMiddleware::AdminMode::Client
Gitlab::SidekiqMiddleware::AdminMode::Client,
Gitlab::SidekiqMiddleware::DuplicateJobs::Client
]
end
......
......@@ -106,4 +106,17 @@ describe Emails::Pipelines do
let(:status_text) { 'Your pipeline has failed.' }
end
end
describe '#pipeline_fixed_email' do
subject { Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email)) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: ref, sha: sha) }
let(:ref) { 'master' }
let(:sha) { project.commit(ref).sha }
it_behaves_like 'correct pipeline information' do
let(:status) { 'been fixed' }
let(:status_text) { 'Your pipeline has been fixed!' }
end
end
end
# 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
)
end
before do
notification_setting.update!(failed_pipeline: true)
it 'returns true' do
expect(recipient.suitable_notification_level?).to eq true
end
end
context "when action is fixed_pipeline" do
let(:recipient) do
described_class.new(
user,
:watch,
custom_action: :fixed_pipeline,
target: target,
project: project
)
end
it 'returns true' do
......@@ -185,7 +197,7 @@ describe NotificationRecipient do
end
end
context "when action is not failed_pipeline" do
context "when action is not fixed_pipeline or failed_pipeline" do
let(:recipient) do
described_class.new(
user,
......@@ -196,10 +208,6 @@ describe NotificationRecipient do
)
end
before do
notification_setting.update!(success_pipeline: true)
end
it 'returns false' do
expect(recipient.suitable_notification_level?).to eq false
end
......@@ -309,6 +317,26 @@ describe NotificationRecipient do
expect(recipient.suitable_notification_level?).to eq false
end
end
context 'when custom_action is fixed_pipeline and success_pipeline event is enabled' do
let(:recipient) do
described_class.new(
user,
:watch,
custom_action: :fixed_pipeline,
target: target,
project: project
)
end
before do
notification_setting.update!(success_pipeline: true)
end
it 'returns true' do
expect(recipient.suitable_notification_level?).to eq true
end
end
end
end
......
......@@ -110,7 +110,8 @@ RSpec.describe NotificationSetting do
:reassign_merge_request,
:merge_merge_request,
:failed_pipeline,
:success_pipeline
:success_pipeline,
:fixed_pipeline
)
end
......
......@@ -72,6 +72,7 @@ describe Project do
it { is_expected.to have_one(:project_setting) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:ci_pipelines) }
it { is_expected.to have_many(:ci_refs) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) }
......
# 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
user = create_user_with_notification(:custom, 'custom_enabled')
update_custom_notification(:success_pipeline, user, resource: project)
update_custom_notification(:failed_pipeline, user, resource: project)
update_custom_notification(:fixed_pipeline, user, resource: project)
user
end
......@@ -2322,6 +2323,7 @@ describe NotificationService, :mailer do
user = create_user_with_notification(:custom, 'custom_disabled')
update_custom_notification(:success_pipeline, user, resource: project, value: false)
update_custom_notification(:failed_pipeline, user, resource: project, value: false)
update_custom_notification(:fixed_pipeline, user, resource: project, value: false)
user
end
......@@ -2514,6 +2516,85 @@ describe NotificationService, :mailer do
end
end
end
context 'with a fixed pipeline' do
let(:ref_status) { 'fixed' }
context 'when the creator has no custom notification set' do
let(:pipeline) { create_pipeline(u_member, :success) }
it 'emails only the creator' do
notification.pipeline_finished(pipeline, ref_status: ref_status)
should_only_email(u_member, kind: :bcc)
end
it_behaves_like 'project emails are disabled' do
let(:notification_target) { pipeline }
let(:notification_trigger) { notification.pipeline_finished(pipeline, ref_status: ref_status) }
end
context 'when the creator has group notification email set' do
let(:group_notification_email) { 'user+group@example.com' }
before do
group = create(:group)
project.update(group: group)
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
end
it 'sends to group notification email' do
notification.pipeline_finished(pipeline, ref_status: ref_status)
expect(email_recipients(kind: :bcc).first).to eq(group_notification_email)
end
end
end
context 'when the creator has watch set' do
before do
pipeline = create_pipeline(u_watcher, :success)
notification.pipeline_finished(pipeline, ref_status: ref_status)
end
it 'emails only the creator' do
should_only_email(u_watcher, kind: :bcc)
end
end
context 'when the creator has custom notifications, but without any set' do
before do
pipeline = create_pipeline(u_custom_notification_unset, :success)
notification.pipeline_finished(pipeline, ref_status: ref_status)
end
it 'emails only the creator' do
should_only_email(u_custom_notification_unset, kind: :bcc)
end
end
context 'when the creator has custom notifications disabled' do
before do
pipeline = create_pipeline(u_custom_notification_disabled, :success)
notification.pipeline_finished(pipeline, ref_status: ref_status)
end
it 'notifies nobody' do
should_not_email_anyone
end
end
context 'when the creator has custom notifications set' do
it 'emails only the creator' do
pipeline = create_pipeline(u_custom_notification_enabled, :success)
notification.pipeline_finished(pipeline, ref_status: ref_status)
should_only_email(u_custom_notification_enabled, kind: :bcc)
end
end
end
end
end
......
......@@ -65,9 +65,16 @@ shared_examples 'resource notes mentions migration' do |migration_class, resourc
end
shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes|
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
end
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
Timecop.freeze do
resource_count = is_for_notes ? Note.count : resource_class.count
expect(resource_count).to eq 5
migrate!
migration = described_class::MIGRATION
......
# 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 @@
require 'spec_helper'
describe 'notify/pipeline_failed_email.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user, developer_projects: [project]) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
user: user,
ref: project.default_branch,
sha: project.commit.sha,
status: :failed)
end
before do
assign(:project, project)
assign(:pipeline, pipeline)
assign(:merge_request, merge_request)
end
shared_examples_for 'renders the pipeline failed email correctly' do
context 'pipeline with user' do
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has failed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content pipeline.user.name
expect(rendered).to have_content build.name
end
it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
context 'pipeline without user' do
before do
pipeline.update_attribute(:user, nil)
end
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has failed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content "by API"
expect(rendered).to have_content build.name
end
end
end
context 'when the pipeline contains a failed job' do
let!(:build) { create(:ci_build, :failed, pipeline: pipeline, project: pipeline.project) }
it_behaves_like 'renders the pipeline failed email correctly'
end
context 'when the latest failed job is a bridge job' do
let!(:build) { create(:ci_bridge, status: :failed, pipeline: pipeline, project: pipeline.project) }
it_behaves_like 'renders the pipeline failed email correctly'
it_behaves_like 'pipeline status changes email' do
let(:title) { 'Your pipeline has failed' }
let(:status) { :failed }
end
end
# 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 @@
require 'spec_helper'
describe 'notify/pipeline_success_email.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user, developer_projects: [project]) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:pipeline) do
create(:ci_pipeline,
project: project,
user: user,
ref: project.default_branch,
sha: project.commit.sha,
status: :success)
end
before do
assign(:project, project)
assign(:pipeline, pipeline)
assign(:merge_request, merge_request)
end
context 'pipeline with user' do
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has passed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content pipeline.user.name
end
it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
context 'pipeline without user' do
before do
pipeline.update_attribute(:user, nil)
end
it 'renders the email correctly' do
render
expect(rendered).to have_content "Your pipeline has passed"
expect(rendered).to have_content pipeline.project.name
expect(rendered).to have_content pipeline.git_commit_message.truncate(50).gsub(/\s+/, ' ')
expect(rendered).to have_content pipeline.commit.author_name
expect(rendered).to have_content "##{pipeline.id}"
expect(rendered).to have_content "by API"
end
it_behaves_like 'pipeline status changes email' do
let(:title) { 'Your pipeline has passed' }
let(:status) { :success }
end
end
......@@ -3,24 +3,8 @@
require 'spec_helper'
describe 'notify/pipeline_success_email.text.erb' do
let(:user) { create(:user, developer_projects: [project]) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:pipeline) do
create(:ci_pipeline,
:success,
project: project,
user: user,
ref: project.default_branch,
sha: project.commit.sha)
end
before do
assign(:project, project)
assign(:pipeline, pipeline)
assign(:merge_request, merge_request)
it_behaves_like 'pipeline status changes email' do
let(:title) { 'Your pipeline has passed' }
let(:status) { :success }
end
it_behaves_like 'correct pipeline information for pipelines for merge requests'
end
......@@ -3,13 +3,16 @@
require 'spec_helper'
describe PipelineNotificationWorker, :mailer do
let(:pipeline) { create(:ci_pipeline) }
let_it_be(:pipeline) { create(:ci_pipeline) }
describe '#execute' do
it 'calls NotificationService#pipeline_finished when the pipeline exists' do
expect(NotificationService).to receive_message_chain(:new, :pipeline_finished)
notification_service_double = double
expect(notification_service_double).to receive(:pipeline_finished)
.with(pipeline, ref_status: 'success', recipients: ['test@gitlab.com'])
expect(NotificationService).to receive(:new).and_return(notification_service_double)
subject.perform(pipeline.id)
subject.perform(pipeline.id, ref_status: 'success', recipients: ['test@gitlab.com'])
end
it 'does nothing when the pipeline does not exist' do
......
# 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