Commit 184c2ced authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 238d22c0
......@@ -130,6 +130,10 @@ export default {
return title;
},
shouldRenderHeaderCallout() {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
},
},
watch: {
// Once the job log is loaded,
......@@ -239,10 +243,9 @@ export default {
/>
</div>
<callout
v-if="shouldRenderCalloutMessage && !hasUnmetPrerequisitesFailure"
:message="job.callout_message"
/>
<callout v-if="shouldRenderHeaderCallout">
<div v-html="job.callout_message"></div>
</callout>
</header>
<!-- EO Header Section -->
......
/**
* Checks if the first argument is a subset of the second argument.
* @param {Set} subset The set to be considered as the subset.
* @param {Set} superset The set to be considered as the superset.
* @returns {boolean}
*/
// eslint-disable-next-line import/prefer-default-export
export const isSubset = (subset, superset) =>
Array.from(subset).every(value => superset.has(value));
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon, GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui';
import {
GlLoadingIcon,
GlButton,
GlTooltipDirective,
GlModal,
GlModalDirective,
GlEmptyState,
} from '@gitlab/ui';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
......@@ -17,6 +24,7 @@ export default {
GlButton,
Icon,
GlModal,
GlEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -103,10 +111,18 @@ export default {
<div v-else-if="!repo.isLoading && isOpen" class="container-image-tags">
<table-registry v-if="repo.list.length" :repo="repo" :can-delete-repo="canDeleteRepo" />
<div v-else class="nothing-here-block">
{{ s__('ContainerRegistry|No tags in Container Registry for this container image.') }}
</div>
<gl-empty-state
v-else
:title="s__('ContainerRegistry|This image has no active tags')"
:description="
s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
)
"
class="mx-auto my-0"
/>
</div>
<gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository">
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
......
......@@ -43,6 +43,7 @@ export default {
},
data() {
return {
selectedItems: [],
itemsToBeDeleted: [],
modalId: `confirm-image-deletion-modal-${this.repo.id}`,
selectAllChecked: false,
......@@ -96,6 +97,7 @@ export default {
},
deleteSingleItem(index) {
this.setModalDescription(index);
this.itemsToBeDeleted = [index];
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
this.removeModalEvents();
......@@ -103,9 +105,10 @@ export default {
});
},
deleteMultipleItems() {
if (this.itemsToBeDeleted.length === 1) {
this.itemsToBeDeleted = [...this.selectedItems];
if (this.selectedItems.length === 1) {
this.setModalDescription(this.itemsToBeDeleted[0]);
} else if (this.itemsToBeDeleted.length > 1) {
} else if (this.selectedItems.length > 1) {
this.setModalDescription();
}
......@@ -115,6 +118,7 @@ export default {
});
},
handleSingleDelete(itemToDelete) {
this.itemsToBeDeleted = [];
this.deleteItem(itemToDelete)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
......@@ -122,6 +126,7 @@ export default {
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
this.selectedItems = [];
if (this.bulkDeletePath) {
this.multiDeleteItems({
......@@ -150,23 +155,23 @@ export default {
}
},
selectAll() {
this.itemsToBeDeleted = this.repo.list.map((x, index) => index);
this.selectedItems = this.repo.list.map((x, index) => index);
this.selectAllChecked = true;
},
deselectAll() {
this.itemsToBeDeleted = [];
this.selectedItems = [];
this.selectAllChecked = false;
},
updateItemsToBeDeleted(index) {
const delIndex = this.itemsToBeDeleted.findIndex(x => x === index);
updateselectedItems(index) {
const delIndex = this.selectedItems.findIndex(x => x === index);
if (delIndex > -1) {
this.itemsToBeDeleted.splice(delIndex, 1);
this.selectedItems.splice(delIndex, 1);
this.selectAllChecked = false;
} else {
this.itemsToBeDeleted.push(index);
this.selectedItems.push(index);
if (this.itemsToBeDeleted.length === this.repo.list.length) {
if (this.selectedItems.length === this.repo.list.length) {
this.selectAllChecked = true;
}
}
......@@ -199,7 +204,7 @@ export default {
v-if="canDeleteRepo"
v-gl-tooltip
v-gl-modal="modalId"
:disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0"
:disabled="!selectedItems || selectedItems.length === 0"
class="js-delete-registry float-right"
data-track-event="click_button"
data-track-label="bulk_registry_tag_delete"
......@@ -219,8 +224,8 @@ export default {
<gl-form-checkbox
v-if="canDeleteRow(item)"
class="js-select-checkbox"
:checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)"
@change="updateItemsToBeDeleted(index)"
:checked="selectedItems && selectedItems.includes(index)"
@change="updateselectedItems(index)"
/>
</td>
<td class="monospace">
......
......@@ -14,13 +14,12 @@
.blank-state-row {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
height: 100%;
justify-content: space-between;
}
.blank-state-welcome {
text-align: center;
padding: 20px 0 40px;
padding: $gl-padding 0 ($gl-padding * 2);
.blank-state-welcome-title {
font-size: 24px;
......@@ -32,23 +31,9 @@
}
.blank-state-link {
display: block;
color: $gl-text-color;
flex: 0 0 100%;
margin-bottom: 15px;
@include media-breakpoint-up(sm) {
flex: 0 0 49%;
&:nth-child(odd) {
margin-right: 5px;
}
&:nth-child(even) {
margin-left: 5px;
}
}
&:hover {
background-color: $gray-light;
text-decoration: none;
......@@ -63,15 +48,25 @@
}
.blank-state {
padding: 20px;
display: flex;
align-items: center;
padding: 20px 50px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
min-height: 240px;
margin-bottom: $gl-padding;
width: calc(50% - #{$gl-padding-8});
@include media-breakpoint-down(sm) {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 50px 20px;
.column-small & {
width: 100%;
}
@include media-breakpoint-up(sm) {
display: flex;
height: 100%;
align-items: center;
padding: 50px 30px;
}
}
......@@ -90,7 +85,7 @@
}
.blank-state-body {
@include media-breakpoint-down(xs) {
@include media-breakpoint-down(sm) {
text-align: center;
margin-top: 20px;
}
......@@ -121,9 +116,3 @@
}
}
}
@include media-breakpoint-down(xs) {
.blank-state-icon svg {
width: 315px;
}
}
......@@ -69,10 +69,6 @@
details {
margin-bottom: $gl-padding;
summary {
margin-bottom: $gl-padding;
}
}
// Single code lines should wrap
......
......@@ -4,6 +4,7 @@ module Groups
class RepositoriesController < Groups::ApplicationController
before_action :verify_container_registry_enabled!
before_action :authorize_read_container_image!
before_action :feature_flag_group_container_registry_browser!
def index
track_event(:list_repositories)
......@@ -22,6 +23,10 @@ module Groups
private
def feature_flag_group_container_registry_browser!
render_404 unless Feature.enabled?(:group_container_registry_browser, group)
end
def verify_container_registry_enabled!
render_404 unless Gitlab.config.registry.enabled
end
......
......@@ -22,7 +22,9 @@ module GroupsHelper
end
def group_container_registry_nav?
Gitlab.config.registry.enabled && can?(current_user, :read_container_image, @group)
Gitlab.config.registry.enabled &&
can?(current_user, :read_container_image, @group) &&
Feature.enabled?(:group_container_registry_browser, @group)
end
def group_sidebar_links
......
# frozen_string_literal: true
module Emails
module Releases
def new_release_email(user_id, release, reason = nil)
@release = release
@project = @release.project
@target_url = namespace_project_releases_url(
namespace_id: @project.namespace,
project_id: @project
)
user = User.find(user_id)
mail(
to: user.notification_email_for(@project.group),
subject: subject(release_email_subject)
)
end
private
def release_email_subject
release_info = [@release.name, @release.tag].select(&:presence).join(' - ')
"New release: #{release_info}"
end
end
end
......@@ -16,6 +16,7 @@ class Notify < BaseMailer
include Emails::Members
include Emails::AutoDevops
include Emails::RemoteMirrors
include Emails::Releases
helper MilestonesHelper
helper MergeRequestsHelper
......
......@@ -754,6 +754,10 @@ module Ci
true
end
def invalid_dependencies
dependencies.reject(&:valid_dependency?)
end
def runner_required_feature_names
strong_memoize(:runner_required_feature_names) do
RUNNER_FEATURES.select do |feature, method|
......
......@@ -25,6 +25,7 @@ class NotificationSetting < ApplicationRecord
end
EMAIL_EVENTS = [
:new_release,
:new_note,
:new_issue,
:reopen_issue,
......
......@@ -26,10 +26,12 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) }
scope :with_project_and_namespace, -> { includes(project: :namespace) }
delegate :repository, to: :project
after_commit :create_evidence!, on: :create
after_commit :notify_new_release, on: :create
def commit
strong_memoize(:commit) do
......@@ -73,6 +75,10 @@ class Release < ApplicationRecord
def create_evidence!
CreateEvidenceWorker.perform_async(self.id)
end
def notify_new_release
NewReleaseWorker.perform_async(id)
end
end
Release.prepend_if_ee('EE::Release')
......@@ -121,4 +121,28 @@ class BuildDetailsEntity < JobEntity
def can_admin_build?
can?(request.current_user, :admin_build, project)
end
def callout_message
return super unless build.failure_reason.to_sym == :missing_dependency_failure
docs_url = "https://docs.gitlab.com/ce/ci/yaml/README.html#dependencies"
[
failure_message.html_safe,
help_message(docs_url).html_safe
].join("<br />")
end
def invalid_dependencies
build.invalid_dependencies.map(&:name).join(', ')
end
def failure_message
_("This job depends on other jobs with expired/erased artifacts: %{invalid_dependencies}") %
{ invalid_dependencies: invalid_dependencies }
end
def help_message(docs_url)
_("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url }
end
end
......@@ -28,6 +28,10 @@ module NotificationRecipientService
Builder::ProjectMaintainers.new(*args).notification_recipients
end
def self.build_new_release_recipients(*args)
Builder::NewRelease.new(*args).notification_recipients
end
module Builder
class Base
def initialize(*)
......@@ -359,6 +363,26 @@ module NotificationRecipientService
end
end
class NewRelease < Base
attr_reader :target
def initialize(target)
@target = target
end
def build!
add_recipients(target.project.authorized_users, :custom, nil)
end
def custom_action
:new_release
end
def acting_user
target.author
end
end
class MergeRequestUnmergeable < Base
attr_reader :target
def initialize(merge_request)
......
......@@ -289,6 +289,15 @@ class NotificationService
end
end
# Notify users when a new release is created
def send_new_release_notifications(release)
recipients = NotificationRecipientService.build_new_release_recipients(release)
recipients.each do |recipient|
mailer.new_release_email(recipient.user.id, release, recipient.reason).deliver_later
end
end
# Members
def new_access_request(member)
return true unless member.notifiable?(:subscription)
......
......@@ -45,12 +45,11 @@
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
.filtered-search-wrapper.d-flex
.filtered-search-box
= dropdown_tag(custom_icon('icon_history'),
= dropdown_tag(_('Recent searches'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
content_class: 'filtered-search-history-dropdown-content' }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
......
.blank-state-row
= link_to new_project_path, class: "blank-state-link" do
.blank-state
- if has_start_trial?
= render_if_exists "dashboard/projects/blank_state_ee_trial"
= link_to new_project_path, class: "blank-state blank-state-link" do
.blank-state-icon
= image_tag("illustrations/welcome/add_new_project")
.blank-state-body
%h3.blank-state-title
Create a project
%p.blank-state-text
Projects are where you store your code, access issues, wiki and other features of GitLab.
- if current_user.can_create_group?
= link_to new_group_path, class: "blank-state blank-state-link" do
.blank-state-icon
= custom_icon("add_new_project", size: 50)
= image_tag("illustrations/welcome/add_new_group")
.blank-state-body
%h3.blank-state-title
Create a project
Create a group
%p.blank-state-text
Projects are where you store your code, access issues, wiki and other features of GitLab.
Groups are a great way to organize projects and people.
- if current_user.can_create_group?
= link_to new_group_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are a great way to organize projects and people.
= link_to new_admin_user_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_user", size: 50)
.blank-state-body
%h3.blank-state-title
Add people
%p.blank-state-text
Add your team members and others to GitLab.
= link_to admin_root_path, class: "blank-state-link" do
.blank-state
= link_to new_admin_user_path, class: "blank-state blank-state-link" do
.blank-state-icon
= custom_icon("configure_server", size: 50)
= image_tag("illustrations/welcome/add_new_user")
.blank-state-body
%h3.blank-state-title
Configure GitLab
Add people
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
Add your team members and others to GitLab.
= link_to admin_root_path, class: "blank-state blank-state-link" do
.blank-state-icon
= image_tag("illustrations/welcome/configure_server")
.blank-state-body
%h3.blank-state-title
Configure GitLab
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
......@@ -2,19 +2,18 @@
.blank-state-row
- if current_user.can_create_project?
= link_to new_project_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body
%h3.blank-state-title
Create a project
%p.blank-state-text
Projects are where you store your code, access issues, wiki and other features of GitLab.
= link_to new_project_path, class: "blank-state blank-state-link" do
.blank-state-icon
= image_tag("illustrations/welcome/add_new_project")
.blank-state-body
%h3.blank-state-title
Create a project
%p.blank-state-text
Projects are where you store your code, access issues, wiki and other features of GitLab.
- else
.blank-state
.blank-state-icon
= custom_icon("add_new_project", size: 50)
= image_tag("illustrations/welcome/add_new_project")
.blank-state-body
%h3.blank-state-title
Create a project
......@@ -22,37 +21,34 @@
If you are added to a project, it will be displayed here.
- if current_user.can_create_group?
= link_to new_group_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "blank-state blank-state-link" do
.blank-state-icon
= image_tag("illustrations/welcome/add_new_group")
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are the best way to manage projects and members.
- if public_project_count > 0
= link_to trending_explore_projects_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("globe", size: 50)
.blank-state-body
%h3.blank-state-title
Explore public projects
%p.blank-state-text
There are
= number_with_delimiter(public_project_count)
public projects on this server.
Public projects are an easy way to allow
everyone to have read-only access.
= link_to "https://docs.gitlab.com/", class: "blank-state-link" do
.blank-state
= link_to trending_explore_projects_path, class: "blank-state blank-state-link" do
.blank-state-icon
= custom_icon("lightbulb", size: 50)
= image_tag("illustrations/welcome/globe")
.blank-state-body
%h3.blank-state-title
Learn more about GitLab
Explore public projects
%p.blank-state-text
Take a look at the documentation to discover all of GitLab's capabilities.
There are
= number_with_delimiter(public_project_count)
public projects on this server.
Public projects are an easy way to allow
everyone to have read-only access.
= link_to "https://docs.gitlab.com/", class: "blank-state blank-state-link" do
.blank-state-icon
= image_tag("illustrations/welcome/lightbulb")
.blank-state-body
%h3.blank-state-title
Learn more about GitLab
%p.blank-state-text
Take a look at the documentation to discover all of GitLab's capabilities.
.blank-state-parent-container{ class: ('has-start-trial-container' if has_start_trial?) }
.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
.row
......@@ -7,12 +7,7 @@
= _('Welcome to GitLab')
%p.blank-state-text
= _('Faster releases. Better code. Less pain.')
.blank-state-row
%div{ class: ('column-large' if has_start_trial?) }
- if current_user.admin?
= render "blank_state_admin_welcome"
- else
= render "blank_state_welcome"
- if has_start_trial?
.column-small
= render_if_exists "blank_state_ee_trial"
- if current_user.admin?
= render "blank_state_admin_welcome"
- else
= render "blank_state_welcome"
- release_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
- description_details = { tag: @release.tag, name: @project.name, release_link_start: release_link_start, release_link_end: '</a>'.html_safe }
%div{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%p
= _("A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it.").html_safe % description_details
%p
%h4= _("Assets:")
%ul
- @release.links.each do |link|
%li= link_to(link.name, link.url)
- @release.sources.each do |source|
%li= link_to(_("Download %{format}") % { format: source.format }, source.url)
%p
%h4= _("Release notes:")
= markdown_field(@release, :description)
<%= _("A new Release %{tag} for %{name} was published. Visit the Releases page to read more about it:").html_safe % { tag: @release.tag, name: @project.name } %> <%= @target_url %>
<%= _("Assets:") %>
<% @release.links.each do |link| -%>
- <%= link.name %>: <%= link.url %>
<% end -%>
<% @release.sources.each do |source| -%>
- <%= _("Download %{format}:") % { format: source.format } %> <%= source.url %>
<% end -%>
<%= _("Release notes:") %>
<%= @release.description %>
......@@ -51,10 +51,10 @@
%hr
.form-group
= f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold'
= f.label :ci_config_path, _('Custom CI configuration path'), class: 'label-bold'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
= _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
= _("The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
......
......@@ -17,12 +17,11 @@
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
.filtered-search-box
- if type != :boards_modal && type != :boards
= dropdown_tag(custom_icon('icon_history'),
= dropdown_tag(_('Recent searches'),
options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown",
content_class: "filtered-search-history-dropdown-content",
title: "Recent searches" }) do
content_class: "filtered-search-history-dropdown-content" }) do
.js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
......
......@@ -119,6 +119,8 @@
- container_repository:delete_container_repository
- container_repository:cleanup_container_repository
- notifications:new_release
- default
- mailers # ActionMailer::DeliveryJob.queue_name
......
# frozen_string_literal: true
class NewReleaseWorker
include ApplicationWorker
queue_namespace :notifications
def perform(release_id)
release = Release.with_project_and_namespace.find_by_id(release_id)
return unless release
NotificationService.new.send_new_release_notifications(release)
end
end
---
title: Add 'New release' to the project custom notifications
merge_request: 17877
author:
type: added
---
title: Use text instead of icon for recent searches dropdown
merge_request:
author:
type: changed
---
title: Add more specific message to clarify the role of empty images in container
registry
merge_request: 32919
author:
type: changed
---
title: Allow to view productivity analytics page without a license
merge_request: 33876
author:
type: fixed
---
title: Fix container registry delete tag modal title and button
merge_request: 34032
author:
type: fixed
---
title: Fix formatting welcome screen external users
merge_request: 16667
author:
type: fixed
---
title: Include in the callout message a list of jobs that caused missing dependencies
failure.
merge_request: 18219
author:
type: added
---
title: 'Geo: Enable replicating uploads, LFS objects, and artifacts in Object Storage'
merge_request: 18482
author:
type: added
......@@ -32,6 +32,10 @@ unless Sidekiq.server?
payload[:response] = event.payload[:response] if event.payload[:response]
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.start_thread_cpu_time)
payload[:cpu_s] = cpu_s
end
payload
end
end
......
......@@ -24,6 +24,7 @@
- [process_commit, 3]
- [new_note, 2]
- [new_issue, 2]
- [notifications, 2]
- [new_merge_request, 2]
- [pipeline_processing, 5]
- [pipeline_creation, 4]
......
# frozen_string_literal: true
class AddNewReleaseToNotificationSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :notification_settings, :new_release, :boolean
end
end
......@@ -2509,6 +2509,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_220135) do
t.boolean "issue_due"
t.boolean "new_epic"
t.string "notification_email"
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
t.index ["user_id"], name: "index_notification_settings_on_user_id"
......
......@@ -198,6 +198,11 @@ separately:
These reference architecture examples rely on the general rule that approximately 2 requests per second (RPS) of load is generated for every 100 users.
The specifications here were performance tested against a specific coded
workload. Your exact needs may be more, depending on your workload. Your
workload is influenced by factors such as - but not limited to - how active your
users are, how much automation you use, mirroring, and repo/change size.
### 10,000 User Configuration
- **Supported Users (approximate):** 10,000
......@@ -211,12 +216,6 @@ environment that supports about 10,000 users. The specifications below are a
representation of the work so far. The specifications may be adjusted in the
future based on additional testing and iteration.
NOTE: **Note:** The specifications here were performance tested against a
specific coded workload. Your exact needs may be more, depending on your
workload. Your workload is influenced by factors such as - but not limited to -
how active your users are, how much automation you use, mirroring, and
repo/change size.
| Service | Configuration | GCP type |
| ------------------------------|-------------------------|----------------|
| 3 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
......
......@@ -211,6 +211,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `relativePosition` | Int | The relative position of the epic in the Epic tree |
| `relationPath` | String | |
| `reference` | String! | |
| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this epic |
### EpicIssue
......
......@@ -122,10 +122,6 @@ Example response:
}
]
},
"_links":{
"merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.2&scope=all&state=opened",
"issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.2&scope=all&state=opened"
}
},
{
"tag_name":"v0.1",
......@@ -182,10 +178,6 @@ Example response:
]
},
"_links":{
"merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
"issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
}
}
]
```
......@@ -297,10 +289,6 @@ Example response:
]
},
"_links":{
"merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
"issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
}
}
```
......@@ -426,10 +414,6 @@ Example response:
}
]
},
"_links":{
"merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.3&scope=all&state=opened",
"issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.3&scope=all&state=opened"
}
}
```
......@@ -531,10 +515,6 @@ Example response:
]
},
"_links":{
"merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
"issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
}
}
```
......@@ -617,10 +597,6 @@ Example response:
]
},
"_links":{
"merge_requests_url": "https://gitlab.example.com/root/awesome_app/merge_requests?release_tag=v0.1&scope=all&state=opened",
"issues_url": "https://gitlab.example.com/root/awesome_app/issues?release_tag=v0.1&scope=all&state=opened"
}
}
```
......
......@@ -85,7 +85,7 @@ GitLab CI/CD supports numerous configuration options:
| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. |
| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
| [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. |
| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. |
| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-configuration-path) | Define a custom path for the CI/CD configuration file. |
| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules.|
| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. |
| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. |
......
---
type: reference
---
# Code Owners **(STARTER)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6916)
in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
> - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-foss/issues/53182) added in GitLab Starter 12.1.
> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
You can use a `CODEOWNERS` file to specify users or
[shared groups](members/share_project_with_groups.md)
......@@ -10,9 +15,9 @@ that are responsible for certain files in a repository.
You can choose and add the `CODEOWNERS` file in three places:
- to the root directory of the repository
- inside the `.gitlab/` directory
- inside the `docs/` directory
- To the root directory of the repository
- Inside the `.gitlab/` directory
- Inside the `docs/` directory
The `CODEOWNERS` file is scoped to a branch, which means that with the
introduction of new files, the person adding the new content can
......@@ -23,6 +28,18 @@ When a file matches multiple entries in the `CODEOWNERS` file,
the users from all entries are displayed on the blob page of
the given file.
## Approvals by Code Owners
Once you've set Code Owners to a project, you can configure it to
receive approvals:
- As [merge request eligible approvers](merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers-starter). **(STARTER)**
- As required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)**
Once set, Code Owners are displayed in merge requests widgets:
![MR widget - Code Owners](img/code_owners_mr_widget_v12_4.png)
## The syntax of Code Owners files
Files can be specified using the same kind of patterns you would use
......
......@@ -101,7 +101,7 @@ any [eligible approver](#eligible-approvers) may approve.
The following can approve merge requests:
- Users being added as approvers at project or merge request level.
- [Code owners](../code_owners.md) related to the merge request ([introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5).
- [Code owners](#code-owners-as-eligible-approvers-starter) to the files changed by the merge request.
An individual user can be added as an approver for a project if they are a member of:
......@@ -119,6 +119,31 @@ if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-thei
and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default)
are enabled on the project settings.
### Code Owners as eligible approvers **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5.
Once you've added [Code Owners](../code_owners.md) to your
repository, the owners to the corresponding files will become
eligible approvers, together with members with Developer or
higher permissions.
To enable this merge request approval rule:
1. Navigate to your project's **Settings > General** and expand
**Merge request approvals**.
1. Locate **All members with Developer role or higher and code owners (if any)** and click **Edit** to choose the number of approvals required.
![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_4.png)
Once set, merge requests can only be merged once approved by the
number of approvals you've set. GitLab will accept approvals from
users with Developer or higher permissions, as well as by Code Owners,
indistinguishably.
Alternatively, you can **require**
[Code Owner's approvals for Protected Branches](../protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)**
### Implicit approvers
If the number of required approvals is greater than the number of approvers,
......@@ -162,26 +187,6 @@ are other conditions that may block it, such as merge conflicts,
[pending discussions](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved)
or a [failed CI/CD pipeline](merge_when_pipeline_succeeds.md).
## Code Owners approvals **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
It is possible to require at least one approval for each entry in the
[`CODEOWNERS` file](../code_owners.md) that matches a file changed in
the merge request. To enable this feature:
1. Navigate to your project's **Settings > General** and expand
**Merge request approvals**.
1. Tick the **Require approval from code owners** checkbox.
1. Click **Save changes**.
When this feature is enabled, all merge requests will need approval
from one code owner per matched rule before it can be merged.
NOTE: **Note:** Only the `CODEOWNERS` file on the default branch is evaluated for
Merge Request approvals. If `CODEOWNERS` is changed on a non-default branch, those
changes will not affect approvals until merged to the default branch.
## Overriding the merge request approvals default settings
> Introduced in GitLab Enterprise Edition 9.4.
......
......@@ -65,14 +65,14 @@ Project defined timeout (either specific timeout set by user or the default
For information about setting a maximum artifact size for a project, see
[Maximum artifacts size](../../admin_area/settings/continuous_integration.md#maximum-artifacts-size-core-only).
## Custom CI config path
## Custom CI configuration path
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4.
By default we look for the `.gitlab-ci.yml` file in the project's root
directory. If you require a different location **within** the repository,
you can set a custom filepath that will be used to lookup the config file,
this filepath should be **relative** to the root.
you can set a custom path that will be used to look up the configuration file,
this path should be **relative** to the root.
Here are some valid examples:
......@@ -85,7 +85,7 @@ The path can be customized at a project level. To customize the path:
1. Go to the project's **Settings > CI / CD**.
1. Expand the **General pipelines** section.
1. Provide a value in the **Custom CI config path** field.
1. Provide a value in the **Custom CI configuration path** field.
1. Click **Save changes**.
## Test coverage parsing
......
......@@ -86,20 +86,6 @@ Click **Protect** and the branch will appear in the "Protected branch" list.
![Roles and users list](img/protected_branches_select_roles_and_users_list.png)
## Code Owners approvals **(PREMIUM)**
It is possible to require at least one approval for each entry in the
[`CODEOWNERS` file](code_owners.md) that matches a file changed in
the merge request. To enable this feature:
1. Toggle the **Require approval from code owners** slider.
1. Click **Protect**.
When this feature is enabled, all merge requests need approval
from one code owner per matched rule before they can be merged. Additionally,
pushes to the protected branch are denied if a rule is matched.
## Wildcard protected branches
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/4665) in GitLab 8.10.
......@@ -166,6 +152,35 @@ Deleting a protected branch is only allowed via the web interface, not via Git.
This means that you can't accidentally delete a protected branch from your
command line or a Git client application.
## Protected Branches approval by Code Owners **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13251) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
It is possible to require at least one approval by a
[Code Owner](code_owners.md) to a file changed by the
merge request. You can either set Code Owners approvals
at the time you protect a new branch, or set it to a branch
already protected, as described below.
To protect a new branch and enable Code Owner's approval:
1. Navigate to your project's **Settings > Repository** and expand **Protected branches**.
1. Scroll down to **Protect a branch**, select a **Branch** or wildcard you'd like to protect, select who's **Allowed to merge** and **Allowed to push**, and toggle the **Require approval from code owners** slider.
1. Click **Protect**.
![Code Owners approval - new protected branch](img/code_owners_approval_new_protected_branch_v12_4.png)
To enable Code Owner's approval to branches already protected:
1. Navigate to your project's **Settings > Repository** and expand **Protected branches**.
1. Scroll down to **Protected branch** and toggle the **Code owner approval** slider for the chosen branch.
![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v12_4.png)
When enabled, all merge requests targeting these branches will require approval
by a Code Owner per matched rule before they can be merged.
Additionally, direct pushes to the protected branch are denied if a rule is matched.
## Running pipelines on protected branches
The permission to merge or push to protected branches is used to define if a user can
......
......@@ -65,6 +65,18 @@ project.
![Releases list](img/releases.png)
## Notification for Releases
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26001) in GitLab 12.4.
You can be notified by email when a new Release is created for your project.
To subscribe to these notifications, navigate to your **Project**'s landing page, then click on the
bell icon. Choose **Custom** from the dropdown menu. The
following modal window will be then displayed, from which you can select **New release** to complete your subscription to new Releases notifications.
![Custom notification - New release](img/custom_notifications_new_release_v12_4.png)
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
......
......@@ -84,6 +84,7 @@ Below is the table of events users can be notified of:
| User added to group | User | Sent when user is added to group |
| Group access level changed | User | Sent when user group access level is changed |
| Project moved | Project members (1) | (1) not disabled |
| New release | Project members | Custom notification |
### Issue / Epics / Merge request events
......
......@@ -1315,8 +1315,8 @@ module API
end
end
expose :_links do
expose :merge_requests_url
expose :issues_url
expose :merge_requests_url, if: -> (_) { release_mr_issue_urls_available? }
expose :issues_url, if: -> (_) { release_mr_issue_urls_available? }
end
private
......@@ -1347,6 +1347,10 @@ module API
{ scope: 'all', state: 'opened', release_tag: object.tag }
end
def release_mr_issue_urls_available?
::Feature.enabled?(:release_mr_issue_urls, project)
end
def project
@project ||= object.project
end
......
......@@ -65,6 +65,8 @@ module Gitlab
end
def self.thread_cpu_time
# Not all OS kernels are supporting `Process::CLOCK_THREAD_CPUTIME_ID`
# Refer: https://gitlab.com/gitlab-org/gitlab/issues/30567#note_221765627
return unless defined?(Process::CLOCK_THREAD_CPUTIME_ID)
Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :float_second)
......
......@@ -6,6 +6,10 @@ module Gitlab
def client_ip
Gitlab::SafeRequestStore[:client_ip]
end
def start_thread_cpu_time
Gitlab::SafeRequestStore[:start_thread_cpu_time]
end
end
def initialize(app)
......@@ -23,6 +27,8 @@ module Gitlab
Gitlab::SafeRequestStore[:client_ip] = req.ip
Gitlab::SafeRequestStore[:start_thread_cpu_time] = Gitlab::Metrics::System.thread_cpu_time
@app.call(env)
end
end
......
......@@ -659,6 +659,12 @@ msgstr ""
msgid "A merge request approval is required when the license compliance report contains a blacklisted license."
msgstr ""
msgid "A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it."
msgstr ""
msgid "A new Release %{tag} for %{name} was published. Visit the Releases page to read more about it:"
msgstr ""
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr ""
......@@ -2009,6 +2015,9 @@ msgstr ""
msgid "Assets"
msgstr ""
msgid "Assets:"
msgstr ""
msgid "Assign"
msgstr ""
......@@ -4358,9 +4367,6 @@ msgstr ""
msgid "ContainerRegistry|Last Updated"
msgstr ""
msgid "ContainerRegistry|No tags in Container Registry for this container image."
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
......@@ -4384,12 +4390,18 @@ msgstr ""
msgid "ContainerRegistry|Tag ID"
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
msgstr ""
msgid "ContainerRegistry|There are no container images available in this group"
msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
......@@ -4813,7 +4825,7 @@ msgstr ""
msgid "CurrentUser|Settings"
msgstr ""
msgid "Custom CI config path"
msgid "Custom CI configuration path"
msgstr ""
msgid "Custom hostname (for private commit emails)"
......@@ -5709,6 +5721,12 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Download %{format}"
msgstr ""
msgid "Download %{format}:"
msgstr ""
msgid "Download CSV"
msgstr ""
......@@ -11139,6 +11157,9 @@ msgstr ""
msgid "NotificationEvent|New note"
msgstr ""
msgid "NotificationEvent|New release"
msgstr ""
msgid "NotificationEvent|Reassign issue"
msgstr ""
......@@ -11969,6 +11990,9 @@ msgstr ""
msgid "Please provide a valid email address."
msgstr ""
msgid "Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>"
msgstr ""
msgid "Please retype the email address."
msgstr ""
......@@ -13492,6 +13516,9 @@ msgstr ""
msgid "Release notes"
msgstr ""
msgid "Release notes:"
msgstr ""
msgid "Release title"
msgstr ""
......@@ -16320,7 +16347,7 @@ msgstr ""
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr ""
msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
msgid "The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>"
msgstr ""
msgid "The phase of the development lifecycle."
......@@ -16728,6 +16755,9 @@ msgstr ""
msgid "This issue is locked."
msgstr ""
msgid "This job depends on other jobs with expired/erased artifacts: %{invalid_dependencies}"
msgstr ""
msgid "This job depends on upstream jobs that need to succeed in order for this job to be triggered"
msgstr ""
......
......@@ -14,3 +14,4 @@ N_('NotificationEvent|Close merge request')
N_('NotificationEvent|Reassign merge request')
N_('NotificationEvent|Merge merge request')
N_('NotificationEvent|Failed pipeline')
N_('NotificationEvent|New release')
import { isSubset } from '~/lib/utils/set';
describe('utils/set', () => {
describe('isSubset', () => {
it.each`
subset | superset | expected
${new Set()} | ${new Set()} | ${true}
${new Set()} | ${new Set([1])} | ${true}
${new Set([1])} | ${new Set([1])} | ${true}
${new Set([1, 3])} | ${new Set([1, 2, 3])} | ${true}
${new Set([1])} | ${new Set()} | ${false}
${new Set([1])} | ${new Set([2])} | ${false}
${new Set([7, 8, 9])} | ${new Set([1, 2, 3])} | ${false}
${new Set([1, 2, 3, 4])} | ${new Set([1, 2, 3])} | ${false}
`('isSubset($subset, $superset) === $expected', ({ subset, superset, expected }) => {
expect(isSubset(subset, superset)).toBe(expected);
});
});
});
......@@ -112,11 +112,13 @@ describe('table registry', () => {
Vue.nextTick(() => {
const deleteBtn = findDeleteButton(wrapper);
expect(wrapper.vm.itemsToBeDeleted).toEqual([0, 1]);
expect(wrapper.vm.selectedItems).toEqual([0, 1]);
expect(deleteBtn.attributes('disabled')).toEqual(undefined);
wrapper.setData({ itemsToBeDeleted: [...wrapper.vm.selectedItems] });
wrapper.vm.handleMultipleDelete();
Vue.nextTick(() => {
expect(wrapper.vm.selectedItems).toEqual([]);
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(wrapper.vm.multiDeleteItems).toHaveBeenCalledWith({
path: bulkDeletePath,
......@@ -143,13 +145,13 @@ describe('table registry', () => {
describe('delete registry', () => {
beforeEach(() => {
wrapper.setData({ itemsToBeDeleted: [0] });
wrapper.setData({ selectedItems: [0] });
});
it('should be possible to delete a registry', () => {
const deleteBtn = findDeleteButton(wrapper);
const deleteBtns = findDeleteButtonsRow(wrapper);
expect(wrapper.vm.itemsToBeDeleted).toEqual([0]);
expect(wrapper.vm.selectedItems).toEqual([0]);
expect(deleteBtn).toBeDefined();
expect(deleteBtn.attributes('disable')).toBe(undefined);
expect(deleteBtns.is('button')).toBe(true);
......@@ -212,15 +214,15 @@ describe('table registry', () => {
describe('modal content', () => {
it('should show the singular title and image name when deleting a single image', () => {
wrapper.setData({ itemsToBeDeleted: [1] });
wrapper.vm.setModalDescription(0);
wrapper.setData({ selectedItems: [1, 2, 3] });
wrapper.vm.deleteSingleItem(0);
expect(wrapper.vm.modalAction).toBe('Remove tag');
expect(wrapper.vm.modalDescription).toContain(firstImage.tag);
});
it('should show the plural title and image count when deleting more than one image', () => {
wrapper.setData({ itemsToBeDeleted: [1, 2] });
wrapper.vm.setModalDescription();
wrapper.setData({ selectedItems: [1, 2] });
wrapper.vm.deleteMultipleItems();
expect(wrapper.vm.modalAction).toBe('Remove tags');
expect(wrapper.vm.modalDescription).toContain('<b>2</b> tags');
......
......@@ -7,6 +7,6 @@ describe GitlabSchema.types['DiffPosition'] do
:new_path, :position_type, :old_line, :new_line, :x, :y,
:width, :height]
is_expected.to have_graphql_field(*expected_fields)
is_expected.to have_graphql_fields(*expected_fields)
end
end
......@@ -34,5 +34,38 @@ describe 'lograge', type: :request do
subject
end
it 'logs cpu_s on supported platform' do
allow(Gitlab::Metrics::System).to receive(:thread_cpu_time)
.and_return(
0.111222333,
0.222333833
)
expect(Lograge.formatter).to receive(:call)
.with(a_hash_including(cpu_s: 0.1111115))
.and_call_original
expect(Lograge.logger).to receive(:send)
.with(anything, include('"cpu_s":0.1111115'))
.and_call_original
subject
end
it 'does not log cpu_s on unsupported platform' do
allow(Gitlab::Metrics::System).to receive(:thread_cpu_time)
.and_return(nil)
expect(Lograge.formatter).to receive(:call)
.with(hash_not_including(:cpu_s))
.and_call_original
expect(Lograge.logger).not_to receive(:send)
.with(anything, include('"cpu_s":'))
.and_call_original
subject
end
end
end
......@@ -40,7 +40,10 @@ describe Gitlab::Cluster::Mixins::PumaCluster do
yield(process.pid)
ensure
Process.kill(:KILL, process.pid) unless process.eof?
begin
Process.kill(:KILL, process.pid)
rescue Errno::ESRCH
end
end
end
end
......
......@@ -54,7 +54,10 @@ describe Gitlab::Cluster::Mixins::UnicornHttpServer do
yield(process.pid)
ensure
Process.kill(:KILL, process.pid) unless process.eof?
begin
Process.kill(:KILL, process.pid)
rescue Errno::ESRCH
end
end
end
end
......
......@@ -58,4 +58,44 @@ describe Gitlab::Metrics::System do
expect(described_class.monotonic_time).to be_an(Float)
end
end
describe '.thread_cpu_time' do
it 'returns cpu_time on supported platform' do
stub_const("Process::CLOCK_THREAD_CPUTIME_ID", 16)
expect(Process).to receive(:clock_gettime)
.with(16, kind_of(Symbol)) { 0.111222333 }
expect(described_class.thread_cpu_time).to eq(0.111222333)
end
it 'returns nil on unsupported platform' do
hide_const("Process::CLOCK_THREAD_CPUTIME_ID")
expect(described_class.thread_cpu_time).to be_nil
end
end
describe '.thread_cpu_duration' do
let(:start_time) { described_class.thread_cpu_time }
it 'returns difference between start and current time' do
stub_const("Process::CLOCK_THREAD_CPUTIME_ID", 16)
expect(Process).to receive(:clock_gettime)
.with(16, kind_of(Symbol))
.and_return(
0.111222333,
0.222333833
)
expect(described_class.thread_cpu_duration(start_time)).to eq(0.1111115)
end
it 'returns nil on unsupported platform' do
hide_const("Process::CLOCK_THREAD_CPUTIME_ID")
expect(described_class.thread_cpu_duration(start_time)).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'email_spec'
describe Emails::Releases do
include EmailSpec::Matchers
include_context 'gitlab email notification'
describe '#new_release_email' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
subject { Notify.new_release_email(user.id, release) }
it_behaves_like 'an email sent from GitLab'
context 'when the release has a name' do
it 'shows the correct subject' do
expected_subject = "#{release.project.name} | New release: #{release.name} - #{release.tag}"
is_expected.to have_subject(expected_subject)
end
end
context 'when the release does not have a name' do
it 'shows the correct subject' do
release.name = nil
expected_subject = "#{release.project.name} | New release: #{release.tag}"
is_expected.to have_subject(expected_subject)
end
end
it 'contains a message with the new release tag' do
message = "A new Release #{release.tag} for #{release.project.name} was published."
is_expected.to have_body_text(message)
end
it 'contains the release assets' do
is_expected.to have_body_text('Assets:')
release.sources do |source|
is_expected.to have_body_text("Download #{source.format}")
end
end
it 'contains the release notes' do
is_expected.to have_body_text('Release notes:')
is_expected.to have_body_text(release.description)
end
end
end
......@@ -3918,4 +3918,14 @@ describe Ci::Build do
end
end
end
describe '#invalid_dependencies' do
let!(:pre_stage_job_valid) { create(:ci_build, :manual, pipeline: pipeline, name: 'test1', stage_idx: 0) }
let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
it 'returns invalid dependencies' do
expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
end
end
end
......@@ -98,6 +98,7 @@ RSpec.describe NotificationSetting do
it 'returns email events' do
expect(subject).to include(
:new_release,
:new_note,
:new_issue,
:reopen_issue,
......
......@@ -109,4 +109,24 @@ RSpec.describe Release do
end
end
end
describe '#notify_new_release' do
context 'when a release is created' do
it 'instantiates NewReleaseWorker to send notifications' do
expect(NewReleaseWorker).to receive(:perform_async)
create(:release)
end
end
context 'when a release is updated' do
let!(:release) { create(:release) }
it 'does not send any new notification' do
expect(NewReleaseWorker).not_to receive(:perform_async)
release.update!(description: 'new description')
end
end
end
end
......@@ -123,6 +123,24 @@ describe BuildDetailsEntity do
end
it { is_expected.to include(failure_reason: 'unmet_prerequisites') }
it { is_expected.to include(callout_message: CommitStatusPresenter.callout_failure_messages[:unmet_prerequisites]) }
end
context 'when the build has failed due to a missing dependency' do
let!(:test1) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test1', stage_idx: 0) }
let!(:test2) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
let!(:build) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
let(:message) { subject[:callout_message] }
before do
build.drop!(:missing_dependency_failure)
end
it { is_expected.to include(failure_reason: 'missing_dependency_failure') }
it 'includes the failing dependencies in the callout message' do
expect(message).to include('test2, test1')
end
end
context 'when a build has environment with latest deployment' do
......
......@@ -678,6 +678,27 @@ describe NotificationService, :mailer do
end
end
describe '#send_new_release_notifications' do
context 'when recipients for a new release exist' do
let(:release) { create(:release) }
it 'calls new_release_email for each relevant recipient' do
user_1 = create(:user)
user_2 = create(:user)
user_3 = create(:user)
recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release)
recipient_2 = NotificationRecipient.new(user_2, :custom, custom_action: :new_release)
allow(NotificationRecipientService).to receive(:build_new_release_recipients).and_return([recipient_1, recipient_2])
release
should_email(user_1)
should_email(user_2)
should_not_email(user_3)
end
end
end
describe 'Participating project notification settings have priority over group and global settings if available' do
let!(:group) { create(:group) }
let!(:maintainer) { group.add_owner(create(:user, username: 'maintainer')).user }
......
......@@ -28,9 +28,15 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end
end
RSpec::Matchers.define :have_graphql_field do |field_name|
RSpec::Matchers.define :have_graphql_field do |field_name, args = {}|
match do |kls|
expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name))
field = kls.fields[GraphqlHelpers.fieldnamerize(field_name)]
expect(field).to be_present
args.each do |argument, value|
expect(field.send(argument)).to eq(value)
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe NewReleaseWorker do
let(:release) { create(:release) }
it 'sends a new release notification' do
expect_any_instance_of(NotificationService).to receive(:send_new_release_notifications).with(release)
described_class.new.perform(release.id)
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