diff options
Diffstat (limited to 'app')
25 files changed, 355 insertions, 182 deletions
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index 1dcdb65d5c7..cc4b2dacab3 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -1,6 +1,7 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; import { n__, __, sprintf } from '~/locale'; +import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; export default { @@ -94,6 +95,9 @@ export default { } return version.versionIndex === -1; }, + isHead() { + return parseBoolean(getParameterByName('diff_head')); + }, isLatest(version) { return ( this.mergeRequestVersion && version.version_index === this.targetVersions[0].version_index @@ -121,7 +125,8 @@ export default { <div> <strong> {{ versionName(version) }} - <template v-if="isBase(version)">{{ + <template v-if="isHead()">{{ s__('DiffsCompareBaseBranch|(HEAD)') }}</template> + <template v-else-if="isBase(version)">{{ s__('DiffsCompareBaseBranch|(base)') }}</template> </strong> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index ffcb0f24cc6..c901971be50 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -134,6 +134,10 @@ export default () => { axios .get(dataset.testReportsCountEndpoint) .then(({ data }) => { + if (!data.total_count) { + return; + } + document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; }) .catch(() => {}); diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index 72867ecd709..fa09e063552 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import { s__, sprintf } from '~/locale'; @@ -43,10 +43,10 @@ You are going to change the username %{currentUsernameBold} to %{newUsernameBold Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible.`), { - currentUsernameBold: `<strong>${_.escape(this.username)}</strong>`, - newUsernameBold: `<strong>${_.escape(this.newUsername)}</strong>`, - currentUsername: _.escape(this.username), - newUsername: _.escape(this.newUsername), + currentUsernameBold: `<strong>${esc(this.username)}</strong>`, + newUsernameBold: `<strong>${esc(this.newUsername)}</strong>`, + currentUsername: esc(this.username), + newUsername: esc(this.newUsername), }, false, ); diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 880e1a88975..55bc9fb8955 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import 'cropper'; -import _ from 'underscore'; +import { isString } from 'lodash'; (() => { // Matches everything but the file name @@ -29,7 +29,7 @@ import _ from 'underscore'; this.onModalShow = this.onModalShow.bind(this); this.onPickImageClick = this.onPickImageClick.bind(this); this.fileInput = $(input); - this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; + this.modalCropImg = isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg; this.fileInput .attr('name', `${this.fileInput.attr('name')}-trigger`) .attr('id', `${this.fileInput.attr('id')}-trigger`); @@ -47,9 +47,9 @@ import _ from 'underscore'; this.filename = this.getElement(filename); this.previewImage = this.getElement(previewImage); this.pickImageEl = this.getElement(pickImageEl); - this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop; - this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; - this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg; + this.modalCrop = isString(modalCrop) ? $(modalCrop) : modalCrop; + this.uploadImageBtn = isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn; + this.modalCropImg = isString(modalCropImg) ? $(modalCropImg) : modalCropImg; this.cropActionsBtn = this.modalCrop.find('[data-method]'); this.bindEvents(); } diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index e27ec571531..dabbcf0eac1 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -65,6 +65,7 @@ // Classes using mixins coming from @gitlab-ui // can be removed once https://gitlab.com/gitlab-org/gitlab/merge_requests/19021 has been merged +.gl-bg-blue-50 { @include gl-bg-blue-50; } .gl-bg-red-100 { @include gl-bg-red-100; } .gl-bg-orange-100 { @include gl-bg-orange-100; } .gl-bg-gray-100 { @include gl-bg-gray-100; } diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index 773b9fead3a..f2538d28a1a 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -10,6 +10,10 @@ module Emails pipeline_mail(pipeline, recipients, 'failed') end + def pipeline_fixed_email(pipeline, recipients) + pipeline_mail(pipeline, recipients, 'been fixed') + end + private def pipeline_mail(pipeline, recipients, status) diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 381a4f54d9e..114737eb232 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -145,6 +145,10 @@ class NotifyPreview < ActionMailer::Preview Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) end + def pipeline_fixed_email + Notify.pipeline_fixed_email(pipeline, pipeline.user.try(:email)) + end + def autodevops_disabled_email Notify.autodevops_disabled_email(pipeline, user.email).message end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 869a2e8da20..e07abc20dcf 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -63,6 +63,14 @@ module Ci has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline + + has_one :ref_status, ->(pipeline) { + # We use .read_attribute to save 1 extra unneeded query to load the :project. + unscope(:where) + .where(project_id: pipeline.read_attribute(:project_id), ref: pipeline.ref, tag: pipeline.tag) + # Sadly :inverse_of is not supported (yet) by Rails for composite PKs. + }, class_name: 'Ci::Ref', inverse_of: :pipelines + has_one :chat_data, class_name: 'Ci::PipelineChatData' has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline @@ -227,7 +235,7 @@ module Ci after_transition any => [:success, :failed] do |pipeline| pipeline.run_after_commit do - PipelineNotificationWorker.perform_async(pipeline.id) + PipelineUpdateCiRefStatusWorker.perform_async(pipeline.id) end end diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb new file mode 100644 index 00000000000..a0782bc0444 --- /dev/null +++ b/app/models/ci/ref.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Ci + class Ref < ApplicationRecord + extend Gitlab::Ci::Model + + STATUSES = %w[success failed fixed].freeze + + belongs_to :project + belongs_to :last_updated_by_pipeline, foreign_key: :last_updated_by_pipeline_id, class_name: 'Ci::Pipeline' + # ActiveRecord doesn't support composite FKs for this reason we have to do the 'unscope(:where)' + # hack. + has_many :pipelines, ->(ref) { + # We use .read_attribute to save 1 extra unneeded query to load the :project. + unscope(:where) + .where(ref: ref.ref, project_id: ref.read_attribute(:project_id), tag: ref.tag) + # Sadly :inverse_of is not supported (yet) by Rails for composite PKs. + }, inverse_of: :ref_status + + validates :status, inclusion: { in: STATUSES } + validates :last_updated_by_pipeline, presence: true + end +end diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 8e44e3d8e17..107d00d055a 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -52,7 +52,8 @@ class NotificationRecipient when :mention @type == :mention when :participating - @custom_action == :failed_pipeline || %i[participating mention].include?(@type) + %i[failed_pipeline fixed_pipeline].include?(@custom_action) || + %i[participating mention].include?(@type) when :custom custom_enabled? || %i[participating mention].include?(@type) when :watch @@ -63,7 +64,13 @@ class NotificationRecipient end def custom_enabled? - @custom_action && notification_setting&.event_enabled?(@custom_action) + return false unless @custom_action + return false unless notification_setting + + notification_setting.event_enabled?(@custom_action) || + # fixed_pipeline is a subset of success_pipeline event + (@custom_action == :fixed_pipeline && + notification_setting.event_enabled?(:success_pipeline)) end def unsubscribed? diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index e2c362538eb..38bd95e6a20 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -44,6 +44,7 @@ class NotificationSetting < ApplicationRecord :reassign_merge_request, :merge_merge_request, :failed_pipeline, + :fixed_pipeline, :success_pipeline ].freeze @@ -76,9 +77,9 @@ class NotificationSetting < ApplicationRecord setting end - # Allow people to receive failed pipeline notifications if they already have - # custom notifications enabled, as these are more like mentions than the other - # custom settings. + # Allow people to receive both failed pipeline/fixed pipeline notifications + # if they already have custom notifications enabled, + # as these are more like mentions than the other custom settings. def failed_pipeline bool = super @@ -86,6 +87,13 @@ class NotificationSetting < ApplicationRecord end alias_method :failed_pipeline?, :failed_pipeline + def fixed_pipeline + bool = super + + bool.nil? || bool + end + alias_method :fixed_pipeline?, :fixed_pipeline + def event_enabled?(event) respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/models/project.rb b/app/models/project.rb index 0f61d32eb8d..41c56fe6931 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -267,6 +267,7 @@ class Project < ApplicationRecord class_name: 'Ci::Pipeline', inverse_of: :project has_many :stages, class_name: 'Ci::Stage', inverse_of: :project + has_many :ci_refs, class_name: 'Ci::Ref' # Ci::Build objects store data on the file system such as artifact files and # build traces. Currently there's no efficient way of removing this data in diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 65bf8535d2a..c3ed958242b 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -49,7 +49,7 @@ class PipelinesEmailService < Service return unless all_recipients.any? pipeline_id = data[:object_attributes][:id] - PipelineNotificationWorker.new.perform(pipeline_id, all_recipients) + PipelineNotificationWorker.new.perform(pipeline_id, recipients: all_recipients) end def can_test? diff --git a/app/services/ci/update_ci_ref_status_service.rb b/app/services/ci/update_ci_ref_status_service.rb new file mode 100644 index 00000000000..e5e5b94b629 --- /dev/null +++ b/app/services/ci/update_ci_ref_status_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Ci + class UpdateCiRefStatusService + include Gitlab::OptimisticLocking + + attr_reader :pipeline + + def initialize(pipeline) + @pipeline = pipeline + end + + def call + save.tap { |success| after_save if success } + end + + private + + def save + might_insert = ref.new_record? + + begin + retry_optimistic_lock(ref) do + next false if ref.persisted? && + (ref.last_updated_by_pipeline_id || 0) >= pipeline.id + + ref.update(status: next_status(ref.status, pipeline.status), + last_updated_by_pipeline: pipeline) + end + rescue ActiveRecord::RecordNotUnique + if might_insert + @ref = pipeline.reset.ref_status + might_insert = false + retry + else + raise + end + end + end + + def next_status(ref_status, pipeline_status) + if ref_status == 'failed' && pipeline_status == 'success' + 'fixed' + else + pipeline_status + end + end + + def after_save + enqueue_pipeline_notification + end + + def enqueue_pipeline_notification + PipelineNotificationWorker.perform_async(pipeline.id, ref_status: ref.status) + end + + def ref + @ref ||= pipeline.ref_status || build_ref + end + + def build_ref + Ci::Ref.new(ref: pipeline.ref, project: pipeline.project, tag: pipeline.tag) + end + end +end diff --git a/app/services/merge_requests/create_pipeline_service.rb b/app/services/merge_requests/create_pipeline_service.rb index 8258efba6bf..f802aa44487 100644 --- a/app/services/merge_requests/create_pipeline_service.rb +++ b/app/services/merge_requests/create_pipeline_service.rb @@ -9,15 +9,10 @@ module MergeRequests end def create_detached_merge_request_pipeline(merge_request) - if can_use_merge_request_ref?(merge_request) - Ci::CreatePipelineService.new(merge_request.source_project, current_user, - ref: merge_request.ref_path) - .execute(:merge_request_event, merge_request: merge_request) - else - Ci::CreatePipelineService.new(merge_request.source_project, current_user, - ref: merge_request.source_branch) - .execute(:merge_request_event, merge_request: merge_request) - end + Ci::CreatePipelineService.new(merge_request.source_project, + current_user, + ref: pipeline_ref_for_detached_merge_request_pipeline(merge_request)) + .execute(:merge_request_event, merge_request: merge_request) end def can_create_pipeline_for?(merge_request) @@ -33,6 +28,16 @@ module MergeRequests def allow_duplicate params[:allow_duplicate] end + + private + + def pipeline_ref_for_detached_merge_request_pipeline(merge_request) + if can_use_merge_request_ref?(merge_request) + merge_request.ref_path + else + merge_request.source_branch + end + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index ac7ef6fb970..6f2bfa8169b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -434,18 +434,19 @@ class NotificationService mailer.project_was_not_exported_email(current_user, project, errors).deliver_later end - def pipeline_finished(pipeline, recipients = nil) + def pipeline_finished(pipeline, ref_status: nil, recipients: nil) # Must always check project configuration since recipients could be a list of emails # from the PipelinesEmailService integration. return if pipeline.project.emails_disabled? - email_template = "pipeline_#{pipeline.status}_email" + ref_status ||= pipeline.status + email_template = "pipeline_#{ref_status}_email" return unless mailer.respond_to?(email_template) recipients ||= notifiable_users( [pipeline.user], :watch, - custom_action: :"#{pipeline.status}_pipeline", + custom_action: :"#{ref_status}_pipeline", target: pipeline ).map do |user| user.notification_email_for(pipeline.project.group) diff --git a/app/views/notify/_successful_pipeline.html.haml b/app/views/notify/_successful_pipeline.html.haml new file mode 100644 index 00000000000..231df2e9206 --- /dev/null +++ b/app/views/notify/_successful_pipeline.html.haml @@ -0,0 +1,118 @@ +- title = local_assigns[:title] +%tr.table-success + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } + %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } + = title +%tr.spacer + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } + +%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;" } + +%tr.success-message + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + triggered by + - if @pipeline.user + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } + %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } + %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } + = @pipeline.user.name + - else + %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } + API +%tr + %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } + - job_count = @pipeline.total_size + - stage_count = @pipeline.stages_count + successfully completed + #{job_count} #{'job'.pluralize(job_count)} + in + #{stage_count} #{'stage'.pluralize(stage_count)}. diff --git a/app/views/notify/_successful_pipeline.text.erb b/app/views/notify/_successful_pipeline.text.erb new file mode 100644 index 00000000000..628976e2dda --- /dev/null +++ b/app/views/notify/_successful_pipeline.text.erb @@ -0,0 +1,32 @@ +<%= local_assigns[:title] %> + +Project: <%= @project.name %> ( <%= project_url(@project) %> ) +Branch: <%= @pipeline.source_ref %> ( <%= commits_url(@pipeline) %> ) +<% if @merge_request -%> +Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> ) +<% end -%> + +Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) +Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> +<% commit = @pipeline.commit -%> +<% if commit.author -%> +Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> ) +<% else -%> +Commit Author: <%= commit.author_name %> +<% end -%> +<% if commit.different_committer? -%> +<% if commit.committer -%> +Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> ) +<% else -%> +Committed by: <%= commit.committer_name %> +<% end -%> +<% end -%> + +<% job_count = @pipeline.total_size -%> +<% stage_count = @pipeline.stages_count -%> +<% if @pipeline.user -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> ) +<% else -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API +<% end -%> +successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. diff --git a/app/views/notify/pipeline_fixed_email.html.haml b/app/views/notify/pipeline_fixed_email.html.haml new file mode 100644 index 00000000000..05c0027a6fc --- /dev/null +++ b/app/views/notify/pipeline_fixed_email.html.haml @@ -0,0 +1 @@ += render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!' diff --git a/app/views/notify/pipeline_fixed_email.text.erb b/app/views/notify/pipeline_fixed_email.text.erb new file mode 100644 index 00000000000..75268531bdc --- /dev/null +++ b/app/views/notify/pipeline_fixed_email.text.erb @@ -0,0 +1 @@ +<%= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!' -%> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index e575a5569fa..c34e02b5fee 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1,117 +1 @@ -%tr.table-success - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } - %img{ alt: "✓", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-check-green-inverted.gif'), style: "display:block;", width: "13" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } - Your pipeline has passed. -%tr.spacer - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } - -%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;" } - -%tr.success-message - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" - triggered by - - if @pipeline.user - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } - %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } - = @pipeline.user.name - - else - %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } - API -%tr - %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } - - job_count = @pipeline.total_size - - stage_count = @pipeline.stages_count - successfully completed - #{job_count} #{'job'.pluralize(job_count)} - in - #{stage_count} #{'stage'.pluralize(stage_count)}. += render 'notify/successful_pipeline', title: 'Your pipeline has passed.' diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index 4005158dc9e..b554bffc908 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -1,32 +1 @@ -Your pipeline has passed. - -Project: <%= @project.name %> ( <%= project_url(@project) %> ) -Branch: <%= @pipeline.source_ref %> ( <%= commits_url(@pipeline) %> ) -<% if @merge_request -%> -Merge Request: <%= @merge_request.to_reference %> ( <%= merge_request_url(@merge_request) %> ) -<% end -%> - -Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) -Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> -<% commit = @pipeline.commit -%> -<% if commit.author -%> -Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> ) -<% else -%> -Commit Author: <%= commit.author_name %> -<% end -%> -<% if commit.different_committer? -%> -<% if commit.committer -%> -Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> ) -<% else -%> -Committed by: <%= commit.committer_name %> -<% end -%> -<% end -%> - -<% job_count = @pipeline.total_size -%> -<% stage_count = @pipeline.stages_count -%> -<% if @pipeline.user -%> -Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> ) -<% else -%> -Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API -<% end -%> -successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. +<%= render 'notify/successful_pipeline', title: 'Your pipeline has passed.' -%> diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index e8682769720..0b7add65d94 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -668,6 +668,13 @@ :resource_boundary: :cpu :weight: 3 :idempotent: +- :name: pipeline_default:pipeline_update_ci_ref_status + :feature_category: :continuous_integration + :has_external_dependencies: + :latency_sensitive: true + :resource_boundary: :cpu + :weight: 3 + :idempotent: - :name: pipeline_hooks:build_hooks :feature_category: :continuous_integration :has_external_dependencies: diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb index e9081cc416f..72663fa19ae 100644 --- a/app/workers/pipeline_notification_worker.rb +++ b/app/workers/pipeline_notification_worker.rb @@ -8,12 +8,20 @@ class PipelineNotificationWorker # rubocop:disable Scalability/IdempotentWorker worker_resource_boundary :cpu # rubocop: disable CodeReuse/ActiveRecord - def perform(pipeline_id, recipients = nil) - pipeline = Ci::Pipeline.find_by(id: pipeline_id) + def perform(pipeline_id, args = {}) + case args + when Hash + ref_status = args[:ref_status] + recipients = args[:recipients] + else # TODO: backward compatible interface, can be removed in 12.10 + recipients = args + ref_status = nil + end + pipeline = Ci::Pipeline.find_by(id: pipeline_id) return unless pipeline - NotificationService.new.pipeline_finished(pipeline, recipients) + NotificationService.new.pipeline_finished(pipeline, ref_status: ref_status, recipients: recipients) end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pipeline_update_ci_ref_status_worker.rb b/app/workers/pipeline_update_ci_ref_status_worker.rb new file mode 100644 index 00000000000..3d6a0d30e9c --- /dev/null +++ b/app/workers/pipeline_update_ci_ref_status_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class PipelineUpdateCiRefStatusWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + include PipelineQueue + + latency_sensitive_worker! + worker_resource_boundary :cpu + + def perform(pipeline_id) + pipeline = Ci::Pipeline.find_by_id(pipeline_id) + + return unless pipeline + + Ci::UpdateCiRefStatusService.new(pipeline).call + end +end |