summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue11
-rw-r--r--app/assets/javascripts/lib/utils/set.js9
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue26
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue29
-rw-r--r--app/assets/stylesheets/framework/blank.scss49
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/controllers/groups/registry/repositories_controller.rb5
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/mailers/emails/releases.rb28
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/notification_setting.rb1
-rw-r--r--app/models/release.rb6
-rw-r--r--app/serializers/build_details_entity.rb24
-rw-r--r--app/services/notification_recipient_service.rb24
-rw-r--r--app/services/notification_service.rb9
-rw-r--r--app/views/admin/runners/index.html.haml5
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml61
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml72
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml15
-rw-r--r--app/views/notify/new_release_email.html.haml18
-rw-r--r--app/views/notify/new_release_email.text.erb12
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/new_release_worker.rb14
-rw-r--r--changelogs/unreleased/26001-notification-release-be.yml5
-rw-r--r--changelogs/unreleased/30619-make-recent-searches-more-visible.yml5
-rw-r--r--changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml6
-rw-r--r--changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml5
-rw-r--r--changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml5
-rw-r--r--changelogs/unreleased/45797-welcome-screen.yml5
-rw-r--r--changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml6
-rw-r--r--changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml5
-rw-r--r--config/initializers/lograge.rb4
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20190930082942_add_new_release_to_notification_settings.rb9
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/high_availability/README.md11
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/releases/index.md24
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/user/project/code_owners.md23
-rwxr-xr-xdoc/user/project/img/code_owners_approval_new_protected_branch_v12_4.pngbin0 -> 141341 bytes
-rwxr-xr-xdoc/user/project/img/code_owners_approval_protected_branch_v12_4.pngbin0 -> 16195 bytes
-rw-r--r--doc/user/project/img/code_owners_mr_widget_v12_4.pngbin0 -> 27875 bytes
-rwxr-xr-xdoc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.pngbin0 -> 26902 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md47
-rw-r--r--doc/user/project/pipelines/settings.md8
-rw-r--r--doc/user/project/protected_branches.md43
-rw-r--r--doc/user/project/releases/img/custom_notifications_new_release_v12_4.pngbin0 -> 65959 bytes
-rw-r--r--doc/user/project/releases/index.md12
-rw-r--r--doc/workflow/notifications.md1
-rw-r--r--lib/api/entities.rb8
-rw-r--r--lib/gitlab/metrics/system.rb2
-rw-r--r--lib/gitlab/request_context.rb6
-rw-r--r--locale/gitlab.pot40
-rw-r--r--locale/unfound_translations.rb1
-rw-r--r--spec/frontend/lib/utils/set_spec.js19
-rw-r--r--spec/frontend/registry/components/table_registry_spec.js16
-rw-r--r--spec/graphql/types/notes/diff_position_type_spec.rb2
-rw-r--r--spec/initializers/lograge_spec.rb33
-rw-r--r--spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb5
-rw-r--r--spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb5
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb40
-rw-r--r--spec/mailers/emails/releases_spec.rb52
-rw-r--r--spec/models/ci/build_spec.rb10
-rw-r--r--spec/models/notification_setting_spec.rb1
-rw-r--r--spec/models/release_spec.rb20
-rw-r--r--spec/serializers/build_details_entity_spec.rb18
-rw-r--r--spec/services/notification_service_spec.rb21
-rw-r--r--spec/support/matchers/graphql_matchers.rb10
-rw-r--r--spec/workers/new_release_worker_spec.rb13
73 files changed, 758 insertions, 235 deletions
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index b4b124d5db1..859f839741f 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -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 -->
diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js
new file mode 100644
index 00000000000..3845d648b61
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/set.js
@@ -0,0 +1,9 @@
+/**
+ * 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));
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index ed48331f459..95f8270b5d0 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,6 +1,13 @@
<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>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index ac7272c4d29..8470fbc2b59 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -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">
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index cbd390e7145..7dd7ab339dd 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -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;
- }
-}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9028bfa8ec9..3876d1c10d4 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -69,10 +69,6 @@
details {
margin-bottom: $gl-padding;
-
- summary {
- margin-bottom: $gl-padding;
- }
}
// Single code lines should wrap
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
index 39f6963ee0a..e09a9e6eb21 100644
--- a/app/controllers/groups/registry/repositories_controller.rb
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -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
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 811467ca03a..6ddcbf61090 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -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
diff --git a/app/mailers/emails/releases.rb b/app/mailers/emails/releases.rb
new file mode 100644
index 00000000000..137858d31e8
--- /dev/null
+++ b/app/mailers/emails/releases.rb
@@ -0,0 +1,28 @@
+# 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
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index d0b43b4397f..c7cfefeec9b 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -16,6 +16,7 @@ class Notify < BaseMailer
include Emails::Members
include Emails::AutoDevops
include Emails::RemoteMirrors
+ include Emails::Releases
helper MilestonesHelper
helper MergeRequestsHelper
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b016aa8e477..c48ab28ce73 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -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|
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 981590b688f..20160da62d2 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -25,6 +25,7 @@ class NotificationSetting < ApplicationRecord
end
EMAIL_EVENTS = [
+ :new_release,
:new_note,
:new_issue,
:reopen_issue,
diff --git a/app/models/release.rb b/app/models/release.rb
index add57367f61..5a7bfe2d495 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -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')
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 0c754157267..480a8cab6ff 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -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
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index fca64270cae..9afbb678f5d 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -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)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ed357aa0392..b56b2cf14e3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -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)
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 545e53e6b09..2bf2b5fce8d 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -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
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index c50b20a83dc..6e7ec1264ea 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,41 +1,40 @@
.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.
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index 8d5bddbb288..e3af3405b76 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -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.
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index eff68f817bb..a2b1f0d9298 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-.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"
diff --git a/app/views/notify/new_release_email.html.haml b/app/views/notify/new_release_email.html.haml
new file mode 100644
index 00000000000..45e99f3c07a
--- /dev/null
+++ b/app/views/notify/new_release_email.html.haml
@@ -0,0 +1,18 @@
+- 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)
diff --git a/app/views/notify/new_release_email.text.erb b/app/views/notify/new_release_email.text.erb
new file mode 100644
index 00000000000..e03cf2d5fd1
--- /dev/null
+++ b/app/views/notify/new_release_email.text.erb
@@ -0,0 +1,12 @@
+<%= _("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 %>
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index a674136e791..66ed1cadf6a 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -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
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 9165147ef2a..9d580930fb8 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -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
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index cd8d1d05d8b..b161cc65602 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -119,6 +119,8 @@
- container_repository:delete_container_repository
- container_repository:cleanup_container_repository
+- notifications:new_release
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb
new file mode 100644
index 00000000000..b80553a60db
--- /dev/null
+++ b/app/workers/new_release_worker.rb
@@ -0,0 +1,14 @@
+# 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
diff --git a/changelogs/unreleased/26001-notification-release-be.yml b/changelogs/unreleased/26001-notification-release-be.yml
new file mode 100644
index 00000000000..f3e81a60dc9
--- /dev/null
+++ b/changelogs/unreleased/26001-notification-release-be.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'New release' to the project custom notifications
+merge_request: 17877
+author:
+type: added
diff --git a/changelogs/unreleased/30619-make-recent-searches-more-visible.yml b/changelogs/unreleased/30619-make-recent-searches-more-visible.yml
new file mode 100644
index 00000000000..c57806fcdd9
--- /dev/null
+++ b/changelogs/unreleased/30619-make-recent-searches-more-visible.yml
@@ -0,0 +1,5 @@
+---
+title: Use text instead of icon for recent searches dropdown
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml b/changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml
new file mode 100644
index 00000000000..76da5fcebc8
--- /dev/null
+++ b/changelogs/unreleased/32919-inform-the-user-that-removing-the-last-tag-of-an-image-it-will-remo.yml
@@ -0,0 +1,6 @@
+---
+title: Add more specific message to clarify the role of empty images in container
+ registry
+merge_request: 32919
+author:
+type: changed
diff --git a/changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml b/changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml
new file mode 100644
index 00000000000..686382c7caf
--- /dev/null
+++ b/changelogs/unreleased/33876-ensure-proper-access-level-check-on-pa.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to view productivity analytics page without a license
+merge_request: 33876
+author:
+type: fixed
diff --git a/changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml b/changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml
new file mode 100644
index 00000000000..a4d3e62a48a
--- /dev/null
+++ b/changelogs/unreleased/34032-container-registry-bug-on-modal-delete-button-and-title-text.yml
@@ -0,0 +1,5 @@
+---
+title: Fix container registry delete tag modal title and button
+merge_request: 34032
+author:
+type: fixed
diff --git a/changelogs/unreleased/45797-welcome-screen.yml b/changelogs/unreleased/45797-welcome-screen.yml
new file mode 100644
index 00000000000..4f0868c484f
--- /dev/null
+++ b/changelogs/unreleased/45797-welcome-screen.yml
@@ -0,0 +1,5 @@
+---
+title: Fix formatting welcome screen external users
+merge_request: 16667
+author:
+type: fixed
diff --git a/changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml b/changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml
new file mode 100644
index 00000000000..eda37da11b4
--- /dev/null
+++ b/changelogs/unreleased/eb-missing-dependencies-custom-callout-message.yml
@@ -0,0 +1,6 @@
+---
+title: Include in the callout message a list of jobs that caused missing dependencies
+ failure.
+merge_request: 18219
+author:
+type: added
diff --git a/changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml b/changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml
new file mode 100644
index 00000000000..8e2c6d4b093
--- /dev/null
+++ b/changelogs/unreleased/mk-remove-flag-geo_object_storage_replication.yml
@@ -0,0 +1,5 @@
+---
+title: 'Geo: Enable replicating uploads, LFS objects, and artifacts in Object Storage'
+merge_request: 18482
+author:
+type: added
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 346725e4080..d5d4c589884 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -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
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 8ca7ab4dcb1..b97e8ad67c9 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -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]
diff --git a/db/migrate/20190930082942_add_new_release_to_notification_settings.rb b/db/migrate/20190930082942_add_new_release_to_notification_settings.rb
new file mode 100644
index 00000000000..2ec5815f542
--- /dev/null
+++ b/db/migrate/20190930082942_add_new_release_to_notification_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddNewReleaseToNotificationSettings < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :notification_settings, :new_release, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cf706f8caaa..8bb2329a0b9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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"
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index de8c3336335..3f842d51d5e 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -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 |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 81fb3b89bcc..d60916851a8 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 79bc3511bc8..ee2df3e4c5d 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -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"
- }
}
```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 0b0b7394748..5286764d178 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -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. |
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 0d422612f02..476f513480c 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -1,8 +1,13 @@
+---
+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
diff --git a/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png b/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png
new file mode 100755
index 00000000000..f813b60dcd9
--- /dev/null
+++ b/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png
Binary files differ
diff --git a/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png b/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png
new file mode 100755
index 00000000000..59da6874d14
--- /dev/null
+++ b/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png
Binary files differ
diff --git a/doc/user/project/img/code_owners_mr_widget_v12_4.png b/doc/user/project/img/code_owners_mr_widget_v12_4.png
new file mode 100644
index 00000000000..7f7b15ee017
--- /dev/null
+++ b/doc/user/project/img/code_owners_mr_widget_v12_4.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png b/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png
new file mode 100755
index 00000000000..c704129685f
--- /dev/null
+++ b/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 5474d0d6ff6..2aa92ba2316 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -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.
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 59e04907e21..6480c7e0af9 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -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
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 1bd272bdd0c..b7c9faeb1df 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -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
diff --git a/doc/user/project/releases/img/custom_notifications_new_release_v12_4.png b/doc/user/project/releases/img/custom_notifications_new_release_v12_4.png
new file mode 100644
index 00000000000..6b4231d5804
--- /dev/null
+++ b/doc/user/project/releases/img/custom_notifications_new_release_v12_4.png
Binary files differ
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index d5ac6f99e7f..ceb077ab8af 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -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
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 9155402e550..d619c870c5e 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 897adef5f58..91811efacd7 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -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
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index cc0cfc8e29e..2a61b3de405 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -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)
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
index ab2549d5e68..13187836e02 100644
--- a/lib/gitlab/request_context.rb
+++ b/lib/gitlab/request_context.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e17d7c29c4b..8e381b31e9f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/locale/unfound_translations.rb b/locale/unfound_translations.rb
index 0826d64049b..1ae0958c43c 100644
--- a/locale/unfound_translations.rb
+++ b/locale/unfound_translations.rb
@@ -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')
diff --git a/spec/frontend/lib/utils/set_spec.js b/spec/frontend/lib/utils/set_spec.js
new file mode 100644
index 00000000000..7636a1c634c
--- /dev/null
+++ b/spec/frontend/lib/utils/set_spec.js
@@ -0,0 +1,19 @@
+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);
+ });
+ });
+});
diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/components/table_registry_spec.js
index 600a7a6ee87..7cb7c012d9d 100644
--- a/spec/frontend/registry/components/table_registry_spec.js
+++ b/spec/frontend/registry/components/table_registry_spec.js
@@ -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');
diff --git a/spec/graphql/types/notes/diff_position_type_spec.rb b/spec/graphql/types/notes/diff_position_type_spec.rb
index 345bca8f702..aa08daaacd4 100644
--- a/spec/graphql/types/notes/diff_position_type_spec.rb
+++ b/spec/graphql/types/notes/diff_position_type_spec.rb
@@ -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
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 24d366731a2..c2c1960eeab 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
index 038b72075ad..1eddf488c5d 100644
--- a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
+++ b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
index 43176e38b2b..2b3a267991c 100644
--- a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
+++ b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 6d2764a06f2..a5aa80686fd 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -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
diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb
new file mode 100644
index 00000000000..19f404db2a6
--- /dev/null
+++ b/spec/mailers/emails/releases_spec.rb
@@ -0,0 +1,52 @@
+# 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
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 26646085921..cd923f50e02 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -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
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 820d233dbdc..094c60e3e09 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -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,
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 64799421eb6..0aac325c2b2 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -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
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index f24036cf0c5..6a84694cee9 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -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
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 70ce05927b4..aa67b87a645 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -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 }
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 4d48b4b5389..d735c10f698 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -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
diff --git a/spec/workers/new_release_worker_spec.rb b/spec/workers/new_release_worker_spec.rb
new file mode 100644
index 00000000000..9010c36f795
--- /dev/null
+++ b/spec/workers/new_release_worker_spec.rb
@@ -0,0 +1,13 @@
+# 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