Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-09 03:09:01 +00:00
parent f44bf01f69
commit 0a9efe0288
63 changed files with 1353 additions and 261 deletions

View file

@ -1,10 +1,17 @@
#import "../fragments/alert_note.fragment.graphql"
mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {
iid,
status,
endedAt
endedAt,
notes {
nodes {
...AlertNote
}
}
}
}
}

View file

@ -50,11 +50,11 @@ export default {
<template>
<!-- 200ms delay so not every mouseover triggers Popover -->
<gl-popover :target="target" :delay="200" boundary="viewport" triggers="hover" placement="top">
<div class="user-popover d-flex">
<div class="p-1 flex-shrink-1">
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
<div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover">
<div class="gl-p-2 flex-shrink-1">
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="gl-mr-3!" />
</div>
<div class="p-1 w-100">
<div class="gl-p-2 gl-w-full">
<template v-if="userIsLoading">
<!-- `gl-skeleton-loading` does not support equal length lines -->
<!-- This can be migrated to `gl-skeleton-loader` when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/872 is completed -->
@ -62,33 +62,31 @@ export default {
v-for="n in $options.maxSkeletonLines"
:key="n"
:lines="1"
class="animation-container-small mb-1"
class="animation-container-small gl-mb-2"
/>
</template>
<template v-else>
<div class="mb-2">
<h5 class="m-0">
<div class="gl-mb-3">
<h5 class="gl-m-0">
{{ user.name }}
</h5>
<span class="text-secondary">@{{ user.username }}</span>
<span class="gl-text-gray-700">@{{ user.username }}</span>
</div>
<div class="text-secondary">
<div v-if="user.bio" class="d-flex mb-1">
<icon name="profile" class="category-icon flex-shrink-0" />
<span ref="bio" class="ml-1">{{ user.bio }}</span>
<div class="gl-text-gray-700">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
<icon name="profile" class="gl-text-gray-600 gl-flex-shrink-0" />
<span ref="bio" class="gl-ml-2">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="d-flex mb-1">
<icon name="work" class="category-icon flex-shrink-0" />
<span ref="workInformation" class="ml-1">{{ user.workInformation }}</span>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
<icon name="work" class="gl-text-gray-600 gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
</div>
<div class="js-location text-secondary d-flex">
<div v-if="user.location">
<icon name="location" class="category-icon flex-shrink-0" />
<span class="ml-1">{{ user.location }}</span>
</div>
<div v-if="user.location" class="js-location gl-text-gray-700 gl-display-flex">
<icon name="location" class="gl-text-gray-600 flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status mt-2">
<div v-if="statusHtml" class="js-user-status gl-mt-3">
<span v-html="statusHtml"></span>
</div>
</template>

View file

@ -100,18 +100,6 @@
}
}
/**
* user_popover component
*/
.user-popover {
padding: $gl-padding-8;
line-height: $gl-line-height;
.category-icon {
color: $gray-600;
}
}
.suggest-gitlab-ci-yml {
margin-top: -1em;

View file

@ -19,8 +19,8 @@ module Mutations
private
def update_status(alert, status)
::AlertManagement::UpdateAlertStatusService
.new(alert, current_user, status)
::AlertManagement::Alerts::UpdateService
.new(alert, current_user, status: status)
.execute
end

View file

@ -31,7 +31,8 @@ module SystemNoteHelper
'designs_added' => 'doc-image',
'designs_modified' => 'doc-image',
'designs_removed' => 'doc-image',
'designs_discussion_added' => 'doc-image'
'designs_discussion_added' => 'doc-image',
'status' => 'status'
}.freeze
def system_note_icon_name(note)

View file

@ -19,6 +19,7 @@ class SystemNoteMetadata < ApplicationRecord
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked outdated
tag due_date pinned_embed cherry_pick health_status approved unapproved
status
].freeze
validates :note, presence: true

View file

@ -12,19 +12,20 @@ module AlertManagement
@alert = alert
@current_user = current_user
@params = params
@param_errors = []
end
def execute
return error_no_permissions unless allowed?
return error_no_updates if params.empty?
filter_assignees
return error_no_assignee_permissions if unauthorized_assignees?
filter_params
return error_invalid_params if param_errors.any?
# Save old assignees for system notes
old_assignees = alert.assignees.to_a
if alert.update(params)
process_assignement(old_assignees)
handle_changes(old_assignees: old_assignees)
success
else
@ -34,7 +35,8 @@ module AlertManagement
private
attr_reader :alert, :current_user, :params
attr_reader :alert, :current_user, :params, :param_errors
delegate :resolved?, to: :alert
def allowed?
current_user&.can?(:update_alert_management_alert, alert)
@ -58,40 +60,80 @@ module AlertManagement
error(_('You have no permissions'))
end
def error_no_updates
error(_('Please provide attributes to update'))
def error_invalid_params
error(param_errors.to_sentence)
end
def error_no_assignee_permissions
error(_('Assignee has no permissions'))
def add_param_error(message)
param_errors << message
end
def filter_params
param_errors << _('Please provide attributes to update') if params.empty?
filter_status
filter_assignees
end
def handle_changes(old_assignees:)
handle_assignement(old_assignees) if params[:assignees]
handle_status_change if params[:status_event]
end
# ----- Assignee-related behavior ------
def unauthorized_assignees?
params[:assignees]&.any? { |user| !user.can?(:read_alert_management_alert, alert) }
end
def filter_assignees
return if params[:assignees].nil?
# Always take first assignee while multiple are not currently supported
params[:assignees] = Array(params[:assignees].first)
param_errors << _('Assignee has no permissions') if unauthorized_assignees?
end
def process_assignement(old_assignees)
def unauthorized_assignees?
params[:assignees]&.any? { |user| !user.can?(:read_alert_management_alert, alert) }
end
def handle_assignement(old_assignees)
assign_todo
add_assignee_system_note(old_assignees)
end
def assign_todo
return if alert.assignees.empty?
todo_service.assign_alert(alert, current_user)
end
def add_assignee_system_note(old_assignees)
SystemNoteService.change_issuable_assignees(alert, alert.project, current_user, old_assignees)
end
# ------ Status-related behavior -------
def filter_status
return unless status = params.delete(:status)
status_key = AlertManagement::Alert::STATUSES.key(status)
status_event = AlertManagement::Alert::STATUS_EVENTS[status_key]
unless status_event
param_errors << _('Invalid status')
return
end
params[:status_event] = status_event
end
def handle_status_change
add_status_change_system_note
resolve_todos if resolved?
end
def add_status_change_system_note
SystemNoteService.change_alert_status(alert, current_user)
end
def resolve_todos
todo_service.resolve_todos_for_target(alert, current_user)
end
end
end
end

View file

@ -1,68 +0,0 @@
# frozen_string_literal: true
module AlertManagement
class UpdateAlertStatusService
include Gitlab::Utils::StrongMemoize
# @param alert [AlertManagement::Alert]
# @param user [User]
# @param status [Integer] Must match a value from AlertManagement::Alert::STATUSES
def initialize(alert, user, status)
@alert = alert
@user = user
@status = status
end
def execute
return error_no_permissions unless allowed?
return error_invalid_status unless status_key
if alert.update(status_event: status_event)
resolve_todos if resolved?
success
else
error(alert.errors.full_messages.to_sentence)
end
end
private
attr_reader :alert, :user, :status
delegate :project, :resolved?, to: :alert
def allowed?
user.can?(:update_alert_management_alert, project)
end
def resolve_todos
TodoService.new.resolve_todos_for_target(alert, user)
end
def status_key
strong_memoize(:status_key) do
AlertManagement::Alert::STATUSES.key(status)
end
end
def status_event
AlertManagement::Alert::STATUS_EVENTS[status_key]
end
def success
ServiceResponse.success(payload: { alert: alert })
end
def error_no_permissions
error(_('You have no permissions'))
end
def error_invalid_status
error(_('Invalid status'))
end
def error(message)
ServiceResponse.error(payload: { alert: alert }, message: message)
end
end
end

View file

@ -292,6 +292,10 @@ module SystemNoteService
merge_requests_service(noteable, noteable.project, user).unapprove_mr
end
def change_alert_status(alert, author)
::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project, author: author).change_alert_status(alert)
end
private
def merge_requests_service(noteable, project, author)

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module SystemNotes
class AlertManagementService < ::SystemNotes::BaseService
# Called when the status of an AlertManagement::Alert has changed
#
# alert - AlertManagement::Alert object.
#
# Example Note text:
#
# "changed the status to Acknowledged"
#
# Returns the created Note object
def change_alert_status(alert)
status = AlertManagement::Alert::STATUSES.key(alert.status).to_s.titleize
body = "changed the status to **#{status}**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Move verify stage usage activity to CE
merge_request: 36090
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Deduplicate labels with identical title and project
merge_request: 21384
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Remove hardcoded reference to gitlab.com in NPM .gitlab-ci.yml template
merge_request: 36124
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add system notes for status updates on alerts
merge_request: 35467
author:
type: added

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
class AddLabelRestoreTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
# copy table
execute "CREATE TABLE #{backup_labels_table_name} (LIKE #{labels_table_name} INCLUDING ALL);"
# make the primary key a real functioning one rather than incremental
execute "ALTER TABLE #{backup_labels_table_name} ALTER COLUMN ID DROP DEFAULT;"
# add some fields that make changes trackable
execute "ALTER TABLE #{backup_labels_table_name} ADD COLUMN restore_action INTEGER;"
execute "ALTER TABLE #{backup_labels_table_name} ADD COLUMN new_title VARCHAR;"
end
def down
drop_table backup_labels_table_name
end
private
def labels_table_name
:labels
end
def backup_labels_table_name
:backup_labels
end
end

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
class AddLabelRestoreForeignKeys < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# create foreign keys
connection.foreign_keys(labels_table_name).each do |fk|
fk_options = fk.options
add_concurrent_foreign_key(backup_labels_table_name, fk.to_table, name: fk.name, column: fk_options[:column])
end
end
def down
connection.foreign_keys(backup_labels_table_name).each do |fk|
with_lock_retries do
remove_foreign_key backup_labels_table_name, name: fk.name
end
end
end
private
def labels_table_name
:labels
end
def backup_labels_table_name
:backup_labels
end
end

View file

@ -0,0 +1,130 @@
# frozen_string_literal: true
class RemoveDuplicateLabelsFromProject < ActiveRecord::Migration[6.0]
DOWNTIME = false
CREATE = 1
RENAME = 2
disable_ddl_transaction!
class BackupLabel < Label
self.table_name = 'backup_labels'
end
class Label < ApplicationRecord
self.table_name = 'labels'
end
class Project < ApplicationRecord
include EachBatch
self.table_name = 'projects'
end
BATCH_SIZE = 100_000
def up
# Split to smaller chunks
# Loop rather than background job, every 100,000
# there are 45,000,000 projects in total
Project.each_batch(of: BATCH_SIZE) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
transaction do
remove_full_duplicates(*range)
end
transaction do
rename_partial_duplicates(*range)
end
end
end
def down
Project.each_batch(of: BATCH_SIZE) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
restore_renamed_labels(*range)
restore_deleted_labels(*range)
end
end
def remove_full_duplicates(start_id, stop_id)
# Fields that are considered duplicate:
# project_id title template description type color
duplicate_labels = ApplicationRecord.connection.execute(<<-SQL.squish)
WITH data AS (
SELECT labels.*,
row_number() OVER (PARTITION BY labels.project_id, labels.title, labels.template, labels.description, labels.type, labels.color ORDER BY labels.id) AS row_number,
#{CREATE} AS restore_action
FROM labels
WHERE labels.project_id BETWEEN #{start_id} AND #{stop_id}
AND NOT EXISTS (SELECT * FROM board_labels WHERE board_labels.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM label_links WHERE label_links.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM label_priorities WHERE label_priorities.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM lists WHERE lists.label_id = labels.id)
AND NOT EXISTS (SELECT * FROM resource_label_events WHERE resource_label_events.label_id = labels.id)
) SELECT * FROM data WHERE row_number > 1;
SQL
if duplicate_labels.any?
# create backup records
BackupLabel.insert_all!(duplicate_labels.map { |label| label.except("row_number") })
Label.where(id: duplicate_labels.pluck("id")).delete_all
end
end
def rename_partial_duplicates(start_id, stop_id)
# We need to ensure that the new title (with `_duplicate#{ID}`) doesn't exceed the limit.
# Truncate the original title (if needed) to 245 characters minus the length of the ID
# then add `_duplicate#{ID}`
soft_duplicates = ApplicationRecord.connection.execute(<<-SQL.squish)
WITH data AS (
SELECT
*,
substring(title from 1 for 245 - length(id::text)) || '_duplicate' || id::text as new_title,
#{RENAME} AS restore_action,
row_number() OVER (PARTITION BY project_id, title ORDER BY id) AS row_number
FROM labels
WHERE project_id BETWEEN #{start_id} AND #{stop_id}
) SELECT * FROM data WHERE row_number > 1;
SQL
if soft_duplicates.any?
# create backup records
BackupLabel.insert_all!(soft_duplicates.map { |label| label.except("row_number") })
ApplicationRecord.connection.execute(<<-SQL.squish)
UPDATE labels SET title = substring(title from 1 for 245 - length(id::text)) || '_duplicate' || id::text
WHERE labels.id IN (#{soft_duplicates.map { |dup| dup["id"] }.join(", ")});
SQL
end
end
def restore_renamed_labels(start_id, stop_id)
# the backup label IDs are not incremental, they are copied directly from the Labels table
ApplicationRecord.connection.execute(<<-SQL.squish)
WITH backups AS (
SELECT id, title
FROM backup_labels
WHERE project_id BETWEEN #{start_id} AND #{stop_id} AND
restore_action = #{RENAME}
) UPDATE labels SET title = backups.title
FROM backups
WHERE labels.id = backups.id;
SQL
end
def restore_deleted_labels(start_id, stop_id)
ActiveRecord::Base.connection.execute(<<-SQL.squish)
INSERT INTO labels
SELECT id, title, color, project_id, created_at, updated_at, template, description, description_html, type, group_id, cached_markdown_version FROM backup_labels
WHERE backup_labels.project_id BETWEEN #{start_id} AND #{stop_id}
AND backup_labels.restore_action = #{CREATE}
SQL
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddUniquenessIndexToLabelTitleAndProject < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
PROJECT_AND_TITLE = [:project_id, :title]
def up
add_concurrent_index :labels, PROJECT_AND_TITLE, where: "labels.group_id IS NULL", unique: true, name: "index_labels_on_project_id_and_title_unique"
remove_concurrent_index :labels, PROJECT_AND_TITLE, name: "index_labels_on_project_id_and_title"
end
def down
add_concurrent_index :labels, PROJECT_AND_TITLE, where: "labels.group_id IS NULL", unique: false, name: "index_labels_on_project_id_and_title"
remove_concurrent_index :labels, PROJECT_AND_TITLE, name: "index_labels_on_project_id_and_title_unique"
end
end

View file

@ -9413,6 +9413,23 @@ CREATE TABLE public.aws_roles (
role_external_id character varying(64) NOT NULL
);
CREATE TABLE public.backup_labels (
id integer NOT NULL,
title character varying,
color character varying,
project_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
template boolean DEFAULT false,
description character varying,
description_html text,
type character varying,
group_id integer,
cached_markdown_version integer,
restore_action integer,
new_title character varying
);
CREATE TABLE public.badges (
id integer NOT NULL,
link_url character varying NOT NULL,
@ -17234,6 +17251,9 @@ ALTER TABLE ONLY public.award_emoji
ALTER TABLE ONLY public.aws_roles
ADD CONSTRAINT aws_roles_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY public.backup_labels
ADD CONSTRAINT backup_labels_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.badges
ADD CONSTRAINT badges_pkey PRIMARY KEY (id);
@ -18368,6 +18388,20 @@ CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approv
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
CREATE INDEX backup_labels_group_id_title_idx ON public.backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer);
CREATE INDEX backup_labels_project_id_idx ON public.backup_labels USING btree (project_id);
CREATE UNIQUE INDEX backup_labels_project_id_title_idx ON public.backup_labels USING btree (project_id, title) WHERE (group_id = NULL::integer);
CREATE INDEX backup_labels_template_idx ON public.backup_labels USING btree (template) WHERE template;
CREATE INDEX backup_labels_title_idx ON public.backup_labels USING btree (title);
CREATE INDEX backup_labels_type_project_id_idx ON public.backup_labels USING btree (type, project_id);
CREATE INDEX ci_builds_gitlab_monitor_metrics ON public.ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
CREATE INDEX code_owner_approval_required ON public.protected_branches USING btree (project_id, code_owner_approval_required) WHERE (code_owner_approval_required = true);
@ -19374,7 +19408,7 @@ CREATE INDEX index_labels_on_group_id_and_title ON public.labels USING btree (gr
CREATE INDEX index_labels_on_project_id ON public.labels USING btree (project_id);
CREATE INDEX index_labels_on_project_id_and_title ON public.labels USING btree (project_id, title) WHERE (group_id = NULL::integer);
CREATE UNIQUE INDEX index_labels_on_project_id_and_title_unique ON public.labels USING btree (project_id, title) WHERE (group_id IS NULL);
CREATE INDEX index_labels_on_template ON public.labels USING btree (template) WHERE template;
@ -21016,6 +21050,9 @@ ALTER TABLE ONLY public.vulnerabilities
ALTER TABLE ONLY public.labels
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.backup_labels
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.merge_requests
ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES public.sprints(id) ON DELETE CASCADE;
@ -22222,6 +22259,9 @@ ALTER TABLE ONLY public.serverless_domain_cluster
ALTER TABLE ONLY public.labels
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.backup_labels
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.project_feature_usages
ADD CONSTRAINT fk_rails_c22a50024b FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@ -23208,6 +23248,10 @@ COPY "schema_migrations" (version) FROM STDIN;
20200304160801
20200304160823
20200304211738
20200305020458
20200305020459
20200305082754
20200305082858
20200305121159
20200305151736
20200305200641

View file

@ -173,7 +173,6 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `geo_repositories_retrying_verification_count` | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | `url` |
| `geo_wikis_retrying_verification_count` | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | `url` |
| `global_search_bulk_cron_queue_size` | Gauge | 12.10 | Number of database records waiting to be synchronized to Elasticsearch | |
| `global_search_awaiting_indexing_queue_size` | Gauge | 13.2 | Number of database updates waiting to be synchronized to Elasticsearch while indexing is paused | |
| `package_files_count` | Gauge | 13.0 | Number of package files on primary | `url` |
| `package_files_checksummed_count` | Gauge | 13.0 | Number of package files checksummed on primary | `url` |
| `package_files_checksum_failed_count` | Gauge | 13.0 | Number of package files failed to calculate the checksum on primary

View file

@ -678,16 +678,16 @@ appear to be associated to any of the services running, since they all appear to
| `releases` | `usage_activity_by_stage` | `release` | | CE+EE | Unique release tags in project |
| `successful_deployments` | `usage_activity_by_stage` | `release` | | CE+EE | Total successful deployments |
| `user_preferences_group_overview_security_dashboard: 0` | `usage_activity_by_stage` | `secure` | | | |
| `ci_builds` | `usage_activity_by_stage` | `verify` | | | Unique builds in project |
| `ci_external_pipelines` | `usage_activity_by_stage` | `verify` | | | Total pipelines in external repositories |
| `ci_internal_pipelines` | `usage_activity_by_stage` | `verify` | | | Total pipelines in GitLab repositories |
| `ci_pipeline_config_auto_devops` | `usage_activity_by_stage` | `verify` | | | Total pipelines from an Auto DevOps template |
| `ci_pipeline_config_repository` | `usage_activity_by_stage` | `verify` | | | Pipelines from templates in repository |
| `ci_pipeline_schedules` | `usage_activity_by_stage` | `verify` | | | Pipeline schedules in GitLab |
| `ci_pipelines` | `usage_activity_by_stage` | `verify` | | | Total pipelines |
| `ci_triggers` | `usage_activity_by_stage` | `verify` | | | Triggers enabled |
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled |
| `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled |
| `ci_builds` | `usage_activity_by_stage` | `verify` | | CE+EE | Unique builds in project |
| `ci_external_pipelines` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines in external repositories |
| `ci_internal_pipelines` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines in GitLab repositories |
| `ci_pipeline_config_auto_devops` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines from an Auto DevOps template |
| `ci_pipeline_config_repository` | `usage_activity_by_stage` | `verify` | | CE+EE | Pipelines from templates in repository |
| `ci_pipeline_schedules` | `usage_activity_by_stage` | `verify` | | CE+EE | Pipeline schedules in GitLab |
| `ci_pipelines` | `usage_activity_by_stage` | `verify` | | CE+EE | Total pipelines |
| `ci_triggers` | `usage_activity_by_stage` | `verify` | | CE+EE | Triggers enabled |
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | CE+EE | Unique clusters with Runner enabled |
| `projects_reporting_ci_cd_back_to_github` | `usage_activity_by_stage` | `verify` | | EE | Unique projects with a GitHub pipeline enabled |
| `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request |
| `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data |
| `application_requests_per_hour` | `topology` | `enablement` | | | Number of requests to the web application per hour |

View file

@ -251,3 +251,16 @@ If you sort by `Priority`, GitLab uses this sort comparison order:
Ties are broken arbitrarily.
![Labels sort priority](img/labels_sort_priority.png)
## Troubleshooting
### Some label titles end with `_duplicate<number>`
In specific circumstances it was possible to create labels with duplicate titles in the same
namespace.
To resolve the duplication, [in GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21384)
and later, some duplicate labels have `_duplicate<number>` appended to their titles.
You can safely change these labels' titles if you prefer.
For details of the original problem, see [issue 30390](https://gitlab.com/gitlab-org/gitlab/issues/30390).

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module ConanPackage
class ConanPackageManifest < Grape::Entity
expose :package_urls, merge: true
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module ConanPackage
class ConanPackageSnapshot < Grape::Entity
expose :package_snapshot, merge: true
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module ConanPackage
class ConanRecipeManifest < Grape::Entity
expose :recipe_urls, merge: true
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module ConanPackage
class ConanRecipeSnapshot < Grape::Entity
expose :recipe_snapshot, merge: true
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module ConanPackage
class ConanUploadUrls < Grape::Entity
expose :upload_urls, merge: true
end
end
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module API
module Entities
module EntityHelpers
def can_read(attr, &block)
->(obj, opts) { Ability.allowed?(opts[:user], "read_#{attr}".to_sym, yield(obj)) }
end
def can_destroy(attr, &block)
->(obj, opts) { Ability.allowed?(opts[:user], "destroy_#{attr}".to_sym, yield(obj)) }
end
def expose_restricted(attr, &block)
expose attr, if: can_read(attr, &block)
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
class NpmPackage < Grape::Entity
expose :name
expose :versions
expose :dist_tags, as: 'dist-tags'
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class NpmPackageTag < Grape::Entity
expose :dist_tags, merge: true
end
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class Dependency < Grape::Entity
expose :id, as: :@id
expose :type, as: :@type
expose :name, as: :id
expose :range
end
end
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class DependencyGroup < Grape::Entity
expose :id, as: :@id
expose :type, as: :@type
expose :target_framework, as: :targetFramework, expose_nil: false
expose :dependencies, using: ::API::Entities::Nuget::Dependency
end
end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class Metadatum < Grape::Entity
expose :project_url, as: :projectUrl, expose_nil: false
expose :license_url, as: :licenseUrl, expose_nil: false
expose :icon_url, as: :iconUrl, expose_nil: false
end
end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class PackageMetadata < Grape::Entity
expose :json_url, as: :@id
expose :archive_url, as: :packageContent
expose :catalog_entry, as: :catalogEntry, using: ::API::Entities::Nuget::PackageMetadataCatalogEntry
end
end
end
end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class PackageMetadataCatalogEntry < Grape::Entity
expose :json_url, as: :@id
expose :authors
expose :dependency_groups, as: :dependencyGroups, using: ::API::Entities::Nuget::DependencyGroup
expose :package_name, as: :id
expose :package_version, as: :version
expose :tags
expose :archive_url, as: :packageContent
expose :summary
expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true
end
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class PackagesMetadata < Grape::Entity
expose :count
expose :items, using: ::API::Entities::Nuget::PackagesMetadataItem
end
end
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class PackagesMetadataItem < Grape::Entity
expose :json_url, as: :@id
expose :lower_version, as: :lower
expose :upper_version, as: :upper
expose :packages_count, as: :count
expose :packages, as: :items, using: ::API::Entities::Nuget::PackageMetadata
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class PackagesVersions < Grape::Entity
expose :versions
end
end
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class SearchResult < Grape::Entity
expose :type, as: :@type
expose :authors
expose :name, as: :id
expose :name, as: :title
expose :summary
expose :total_downloads, as: :totalDownloads
expose :verified
expose :version
expose :versions, using: ::API::Entities::Nuget::SearchResultVersion
expose :tags
expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true
end
end
end
end

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class SearchResultVersion < Grape::Entity
expose :json_url, as: :@id
expose :version
expose :downloads
end
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class SearchResults < Grape::Entity
expose :total_count, as: :totalHits
expose :data, using: ::API::Entities::Nuget::SearchResult
end
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module Nuget
class ServiceIndex < Grape::Entity
expose :version
expose :resources
end
end
end
end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
module API
module Entities
class Package < Grape::Entity
include ::API::Helpers::RelatedResourcesHelpers
extend ::API::Entities::EntityHelpers
expose :id
expose :name
expose :version
expose :package_type
expose :_links do
expose :web_path do |package|
::Gitlab::Routing.url_helpers.project_package_path(package.project, package)
end
expose :delete_api_path, if: can_destroy(:package, &:project) do |package|
expose_url api_v4_projects_packages_path(package_id: package.id, id: package.project_id)
end
end
expose :created_at
expose :project_id, if: ->(_, opts) { opts[:group] }
expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) }
expose :tags
expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
expose :versions, using: ::API::Entities::PackageVersion
private
def project_path
object.project.full_path
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
class Package < Grape::Entity
class Pipeline < ::API::Entities::PipelineBasic
expose :user, using: ::API::Entities::UserBasic
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
class PackageFile < Grape::Entity
expose :id, :package_id, :created_at
expose :file_name, :size
expose :file_md5, :file_sha1
end
end
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
class PackageVersion < Grape::Entity
expose :id
expose :version
expose :created_at
expose :tags
expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
end
end
end

View file

@ -34,9 +34,9 @@ create_npmrc:
echo 'No .npmrc found! Creating one now. Please review the following link for more information: https://docs.gitlab.com/ee/user/packages/npm_registry/index.html#authenticating-with-a-ci-job-token'
{
echo "@${CI_PROJECT_ROOT_NAMESPACE}:registry=\${CI_SERVER_URL}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/"
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}'
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}'
echo '@${CI_PROJECT_ROOT_NAMESPACE}:registry=${CI_SERVER_PROTOCOL}://${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/'
echo '//${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}'
echo '//${CI_SERVER_HOST}:${CI_SERVER_PORT}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}'
} >> .npmrc
fi

View file

@ -532,9 +532,21 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
# Omitted because no user, creator or author associated: `ci_runners`
# rubocop: disable CodeReuse/ActiveRecord
def usage_activity_by_stage_verify(time_period)
{}
{
ci_builds: distinct_count(::Ci::Build.where(time_period), :user_id),
ci_external_pipelines: distinct_count(::Ci::Pipeline.external.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
ci_internal_pipelines: distinct_count(::Ci::Pipeline.internal.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
ci_pipeline_config_auto_devops: distinct_count(::Ci::Pipeline.auto_devops_source.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
ci_pipeline_config_repository: distinct_count(::Ci::Pipeline.repository_source.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
ci_pipeline_schedules: distinct_count(::Ci::PipelineSchedule.where(time_period), :owner_id),
ci_pipelines: distinct_count(::Ci::Pipeline.where(time_period), :user_id, start: user_minimum_id, finish: user_maximum_id),
ci_triggers: distinct_count(::Ci::Trigger.where(time_period), :owner_id),
clusters_applications_runner: cluster_applications_user_distinct_count(::Clusters::Applications::Runner, time_period)
}
end
# rubocop: enable CodeReuse/ActiveRecord
# Currently too complicated and to get reliable counts for these stats:
# container_scanning_jobs, dast_jobs, dependency_scanning_jobs, license_management_jobs, sast_jobs, secret_detection_jobs

View file

@ -23533,6 +23533,9 @@ msgstr ""
msgid "This content could not be displayed because %{reason}. You can %{options} instead."
msgstr ""
msgid "This credential has expired"
msgstr ""
msgid "This date is after the due date, so this epic won't appear in the roadmap."
msgstr ""

View file

@ -19,7 +19,7 @@ RSpec.describe 'User sees user popover', :js do
subject { page }
describe 'hovering over a user link in a merge request' do
let(:popover_selector) { '.user-popover' }
let(:popover_selector) { '[data-testid="user-popover"]' }
before do
visit project_merge_request_path(project, merge_request)

View file

@ -7,22 +7,23 @@ export * from '@gitlab/ui';
*
* This mock decouples those tests from the implementation, removing the need to set
* them up specially just for these tooltips.
*
* Mocking the modules using the full file path allows the mocks to take effect
* when the modules are imported in this project (`gitlab`) **and** when they
* are imported internally in `@gitlab/ui`.
*/
export const GlTooltipDirective = {
bind() {},
};
export const GlTooltip = {
jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
bind() {},
}));
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
render(h) {
return h('div', this.$attrs, this.$slots.default);
},
};
}));
export const GlPopoverDirective = {
bind() {},
};
export const GlPopover = {
jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
props: {
cssClasses: {
type: Array,
@ -33,4 +34,4 @@ export const GlPopover = {
render(h) {
return h('div', this.$attrs, Object.keys(this.$slots).map(s => this.$slots[s]));
},
};
}));

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Nuget::DependencyGroup do
let(:dependency_group) do
{
id: 'http://gitlab.com/Sandbox.App/1.0.0.json#dependencygroup',
type: 'PackageDependencyGroup',
target_framework: 'fwk test',
dependencies: [
{
id: 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
type: 'PackageDependency',
name: 'Dependency',
range: '2.0.0'
}
]
}
end
let(:expected) do
{
'@id': 'http://gitlab.com/Sandbox.App/1.0.0.json#dependencygroup',
'@type': 'PackageDependencyGroup',
'targetFramework': 'fwk test',
'dependencies': [
{
'@id': 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
'@type': 'PackageDependency',
'id': 'Dependency',
'range': '2.0.0'
}
]
}
end
let(:entity) { described_class.new(dependency_group) }
subject { entity.as_json }
it { is_expected.to eq(expected) }
context 'dependency group without target framework' do
let(:dependency_group_with_no_target_framework) { dependency_group.tap { |dg| dg[:target_framework] = nil } }
let(:expected_no_target_framework) { expected.except(:targetFramework) }
let(:entity) { described_class.new(dependency_group_with_no_target_framework) }
it { is_expected.to eq(expected_no_target_framework) }
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Nuget::Dependency do
let(:dependency) do
{
id: 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
type: 'PackageDependency',
name: 'Dependency',
range: '2.0.0'
}
end
let(:expected) do
{
'@id': 'http://gitlab.com/Sandbox.App/1.0.0.json#dependency',
'@type': 'PackageDependency',
'id': 'Dependency',
'range': '2.0.0'
}
end
let(:entity) { described_class.new(dependency) }
subject { entity.as_json }
it { is_expected.to eq(expected) }
end

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Nuget::Metadatum do
let(:metadatum) do
{
project_url: 'http://sandbox.com/project',
license_url: 'http://sandbox.com/license',
icon_url: 'http://sandbox.com/icon'
}
end
let(:expected) do
{
'projectUrl': 'http://sandbox.com/project',
'licenseUrl': 'http://sandbox.com/license',
'iconUrl': 'http://sandbox.com/icon'
}
end
let(:entity) { described_class.new(metadatum) }
subject { entity.as_json }
it { is_expected.to eq(expected) }
%i[project_url license_url icon_url].each do |optional_field|
context "metadatum without #{optional_field}" do
let(:metadatum_without_a_field) { metadatum.except(optional_field) }
let(:expected_without_a_field) { expected.except(optional_field.to_s.camelize(:lower).to_sym) }
let(:entity) { described_class.new(metadatum_without_a_field) }
it { is_expected.to eq(expected_without_a_field) }
end
end
end

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Nuget::PackageMetadataCatalogEntry do
let(:entry) do
{
json_url: 'http://sandbox.com/json/package',
authors: 'Authors',
dependency_groups: [],
package_name: 'PackageTest',
package_version: '1.2.3',
tags: 'tag1 tag2 tag3',
archive_url: 'http://sandbox.com/archive/package',
summary: 'Summary',
metadatum: {
project_url: 'http://sandbox.com/project',
license_url: 'http://sandbox.com/license',
icon_url: 'http://sandbox.com/icon'
}
}
end
let(:expected) do
{
'@id': 'http://sandbox.com/json/package',
'id': 'PackageTest',
'version': '1.2.3',
'authors': 'Authors',
'dependencyGroups': [],
'tags': 'tag1 tag2 tag3',
'packageContent': 'http://sandbox.com/archive/package',
'summary': 'Summary',
'projectUrl': 'http://sandbox.com/project',
'licenseUrl': 'http://sandbox.com/license',
'iconUrl': 'http://sandbox.com/icon'
}
end
subject { described_class.new(entry).as_json }
it { is_expected.to eq(expected) }
end

View file

@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::Nuget::SearchResult do
let(:search_result) do
{
type: 'Package',
authors: 'Author',
name: 'PackageTest',
version: '1.2.3',
versions: [
{
json_url: 'http://sandbox.com/json/package',
downloads: 100,
version: '1.2.3'
}
],
summary: 'Summary',
total_downloads: 100,
verified: true,
tags: 'tag1 tag2 tag3',
metadatum: {
project_url: 'http://sandbox.com/project',
license_url: 'http://sandbox.com/license',
icon_url: 'http://sandbox.com/icon'
}
}
end
let(:expected) do
{
'@type': 'Package',
'authors': 'Author',
'id': 'PackageTest',
'title': 'PackageTest',
'summary': 'Summary',
'totalDownloads': 100,
'verified': true,
'version': '1.2.3',
'tags': 'tag1 tag2 tag3',
'projectUrl': 'http://sandbox.com/project',
'licenseUrl': 'http://sandbox.com/license',
'iconUrl': 'http://sandbox.com/icon',
'versions': [
{
'@id': 'http://sandbox.com/json/package',
'downloads': 100,
'version': '1.2.3'
}
]
}
end
subject { described_class.new(search_result).as_json }
it { is_expected.to eq(expected) }
end

View file

@ -119,6 +119,45 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
]
end
end
context 'for verify' do
it 'includes accurate usage_activity_by_stage data' do
for_defined_days_back do
user = create(:user)
create(:ci_build, user: user)
create(:ci_empty_pipeline, source: :external, user: user)
create(:ci_empty_pipeline, user: user)
create(:ci_pipeline, :auto_devops_source, user: user)
create(:ci_pipeline, :repository_source, user: user)
create(:ci_pipeline_schedule, owner: user)
create(:ci_trigger, owner: user)
create(:clusters_applications_runner, :installed)
end
expect(described_class.uncached_data[:usage_activity_by_stage][:verify]).to include(
ci_builds: 2,
ci_external_pipelines: 2,
ci_internal_pipelines: 2,
ci_pipeline_config_auto_devops: 2,
ci_pipeline_config_repository: 2,
ci_pipeline_schedules: 2,
ci_pipelines: 2,
ci_triggers: 2,
clusters_applications_runner: 2
)
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:verify]).to include(
ci_builds: 1,
ci_external_pipelines: 1,
ci_internal_pipelines: 1,
ci_pipeline_config_auto_devops: 1,
ci_pipeline_config_repository: 1,
ci_pipeline_schedules: 1,
ci_pipelines: 1,
ci_triggers: 1,
clusters_applications_runner: 1
)
end
end
end
context 'for create' do

View file

@ -0,0 +1,238 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200305082754_remove_duplicate_labels_from_project.rb')
RSpec.describe RemoveDuplicateLabelsFromProject do
let(:labels_table) { table(:labels) }
let(:labels) { labels_table.all }
let(:projects_table) { table(:projects) }
let(:projects) { projects_table.all }
let(:namespaces_table) { table(:namespaces) }
let(:namespaces) { namespaces_table.all }
let(:backup_labels_table) { table(:backup_labels) }
let(:backup_labels) { backup_labels_table.all }
# all the possible tables with records that may have a relationship with a label
let(:analytics_cycle_analytics_group_stages_table) { table(:analytics_cycle_analytics_group_stages) }
let(:analytics_cycle_analytics_project_stages_table) { table(:analytics_cycle_analytics_project_stages) }
let(:board_labels_table) { table(:board_labels) }
let(:label_links_table) { table(:label_links) }
let(:label_priorities_table) { table(:label_priorities) }
let(:lists_table) { table(:lists) }
let(:resource_label_events_table) { table(:resource_label_events) }
let!(:group_one) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
let!(:project_one) do
projects_table.create!(id: 1, name: 'project', path: 'project',
visibility_level: 0, namespace_id: group_one.id)
end
let(:label_title) { 'bug' }
let(:label_color) { 'red' }
let(:label_description) { 'nice label' }
let(:group_id) { group_one.id }
let(:project_id) { project_one.id }
let(:other_title) { 'feature' }
let(:group_label_attributes) do
{
title: label_title, color: label_color, group_id: group_id, type: 'GroupLabel', template: false, description: label_description
}
end
let(:project_label_attributes) do
{
title: label_title, color: label_color, project_id: project_id, type: 'ProjectLabel', template: false, description: label_description
}
end
let(:migration) { described_class.new }
describe 'removing full duplicates' do
context 'when there are no duplicate labels' do
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, title: "a different label")) }
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, title: "a totally different label")) }
it 'does not remove anything' do
expect { migration.up }.not_to change { backup_labels_table.count }
end
it 'restores removed records when rolling back - no change' do
migration.up
expect { migration.down }.not_to change { labels_table.count }
end
end
context 'with duplicates with no relationships' do
# can't use the activerecord class because the `type` makes it think it has polymorphism and should be/have a ProjectLabel subclass
let(:backup_labels) { ApplicationRecord.connection.execute('SELECT * from backup_labels') }
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3, title: other_title)) }
let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4, title: other_title)) }
it 'creates a backup record for each removed record' do
expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
end
it 'creates the correct backup records with `create` restore_action' do
migration.up
expect(backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
expect(backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
end
it 'deletes all but one' do
migration.up
expect { second_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { fourth_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'restores removed records on rollback' do
second_label_attributes = modified_attributes(second_label)
fourth_label_attributes = modified_attributes(fourth_label)
migration.up
migration.down
expect(second_label.attributes).to include(second_label_attributes)
expect(fourth_label.attributes).to include(fourth_label_attributes)
end
end
context 'two duplicate records, one of which has a relationship' do
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
let!(:label_priority) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
it 'does not remove anything' do
expect { migration.up }.not_to change { labels_table.count }
end
it 'does not create a backup record with `create` restore_action' do
expect { migration.up }.not_to change { backup_labels_table.where(restore_action: described_class::CREATE).count }
end
it 'restores removed records when rolling back - no change' do
migration.up
expect { migration.down }.not_to change { labels_table.count }
end
end
context 'multiple duplicates, a subset of which have relationships' do
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1)) }
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2)) }
let!(:label_priority_for_second_label) { label_priorities_table.create!(label_id: second_label.id, project_id: project_id, priority: 1) }
let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3)) }
let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4)) }
let!(:label_priority_for_fourth_label) { label_priorities_table.create!(label_id: fourth_label.id, project_id: project_id, priority: 2) }
it 'creates a backup record with `create` restore_action for each removed record' do
expect { migration.up }.to change { backup_labels_table.where(restore_action: described_class::CREATE).count }.from(0).to(1)
end
it 'creates the correct backup records' do
migration.up
# can't use the activerecord class because the `type` column makes it think it has polymorphism and should be/have a ProjectLabel subclass
backup_labels = ApplicationRecord.connection.execute('SELECT * from backup_labels')
expect(backup_labels.find { |bl| bl["id"] == 3 }).to include(third_label.attributes.merge("restore_action" => described_class::CREATE, "new_title" => nil, "created_at" => anything, "updated_at" => anything))
end
it 'deletes the duplicate record' do
migration.up
expect { first_label.reload }.not_to raise_error
expect { second_label.reload }.not_to raise_error
expect { third_label.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'restores removed records on rollback' do
third_label_attributes = modified_attributes(third_label)
migration.up
migration.down
expect(third_label.attributes).to include(third_label_attributes)
end
end
end
describe 'renaming partial duplicates' do
# partial duplicates - only project_id and title match. Distinct colour prevents deletion.
context 'when there are no duplicate labels' do
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, title: "a unique label", color: 'green')) }
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, title: "a totally different, unique, label", color: 'blue')) }
it 'does not rename anything' do
expect { migration.up }.not_to change { backup_labels_table.count }
end
end
context 'with duplicates with no relationships' do
let!(:first_label) { labels_table.create!(project_label_attributes.merge(id: 1, color: 'green')) }
let!(:second_label) { labels_table.create!(project_label_attributes.merge(id: 2, color: 'blue')) }
let!(:third_label) { labels_table.create!(project_label_attributes.merge(id: 3, title: other_title, color: 'purple')) }
let!(:fourth_label) { labels_table.create!(project_label_attributes.merge(id: 4, title: other_title, color: 'yellow')) }
it 'creates a backup record for each renamed record' do
expect { migration.up }.to change { backup_labels_table.count }.from(0).to(2)
end
it 'creates the correct backup records with `rename` restore_action' do
migration.up
# can't use the activerecord class because the `type` makes it think it has polymorphism and should be/have a ProjectLabel subclass
backup_labels = ApplicationRecord.connection.execute('SELECT * from backup_labels')
expect(backup_labels.find { |bl| bl["id"] == 2 }).to include(second_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
expect(backup_labels.find { |bl| bl["id"] == 4 }).to include(fourth_label.attributes.merge("restore_action" => described_class::RENAME, "created_at" => anything, "updated_at" => anything))
end
it 'modifies the titles of the partial duplicates' do
migration.up
expect(second_label.reload.title).to match(/#{label_title}_duplicate#{second_label.id}$/)
expect(fourth_label.reload.title).to match(/#{other_title}_duplicate#{fourth_label.id}$/)
end
it 'restores renamed records on rollback' do
second_label_attributes = modified_attributes(second_label)
fourth_label_attributes = modified_attributes(fourth_label)
migration.up
migration.down
expect(second_label.reload.attributes).to include(second_label_attributes)
expect(fourth_label.reload.attributes).to include(fourth_label_attributes)
end
context 'when the labels have a long title that might overflow' do
let(:long_title) { "a" * 255 }
before do
first_label.update_attribute(:title, long_title)
second_label.update_attribute(:title, long_title)
end
it 'keeps the length within the limit' do
migration.up
expect(second_label.reload.title).to eq("#{"a" * 244}_duplicate#{second_label.id}")
expect(second_label.title.length).to eq 255
end
end
end
end
def modified_attributes(label)
label.attributes.except('created_at', 'updated_at')
end
end

View file

@ -6,7 +6,7 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
let_it_be(:user_with_permissions) { create(:user) }
let_it_be(:other_user_with_permissions) { create(:user) }
let_it_be(:user_without_permissions) { create(:user) }
let_it_be(:alert, reload: true) { create(:alert_management_alert) }
let_it_be(:alert, reload: true) { create(:alert_management_alert, :triggered) }
let_it_be(:project) { alert.project }
let(:current_user) { user_with_permissions }
@ -28,6 +28,10 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
specify { expect { response }.not_to change(Note, :count) }
end
shared_examples 'adds a system note' do
specify { expect { response }.to change { alert.reload.notes.count }.by(1) }
end
shared_examples 'error response' do |message|
it_behaves_like 'does not add a todo'
it_behaves_like 'does not add a system note'
@ -86,10 +90,6 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
end
end
shared_examples 'adds a system note' do
specify { expect { response }.to change { alert.reload.notes.count }.by(1) }
end
shared_examples 'successful assignment' do
it_behaves_like 'adds a system note'
it_behaves_like 'adds a todo'
@ -143,5 +143,38 @@ RSpec.describe AlertManagement::Alerts::UpdateService do
it_behaves_like 'successful assignment'
end
end
context 'when a status is included' do
let(:params) { { status: new_status } }
let(:new_status) { AlertManagement::Alert::STATUSES[:acknowledged] }
it 'successfully changes the status' do
expect { response }.to change { alert.acknowledged? }.to(true)
expect(response).to be_success
expect(response.payload[:alert]).to eq(alert)
end
it_behaves_like 'adds a system note'
context 'with unknown status' do
let(:new_status) { -1 }
it_behaves_like 'error response', 'Invalid status'
end
context 'with resolving status' do
let(:new_status) { AlertManagement::Alert::STATUSES[:resolved] }
it 'changes the status' do
expect { response }.to change { alert.resolved? }.to(true)
end
it "resolves the current user's related todos" do
todo = create(:todo, :pending, target: alert, user: current_user, project: alert.project)
expect { response }.to change { todo.reload.state }.from('pending').to('done')
end
end
end
end
end

View file

@ -1,83 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AlertManagement::UpdateAlertStatusService do
let(:project) { alert.project }
let_it_be(:user) { build(:user) }
let_it_be(:alert, reload: true) do
create(:alert_management_alert, :triggered)
end
let(:service) { described_class.new(alert, user, new_status) }
describe '#execute' do
shared_examples 'update failure' do |error_message|
it 'returns an error' do
expect(response).to be_error
expect(response.message).to eq(error_message)
expect(response.payload[:alert]).to eq(alert)
end
it 'does not update the status' do
expect { response }.not_to change { alert.status }
end
end
let(:new_status) { AlertManagement::Alert::STATUSES[:acknowledged] }
let(:can_update) { true }
subject(:response) { service.execute }
before do
allow(user).to receive(:can?)
.with(:update_alert_management_alert, project)
.and_return(can_update)
end
it 'returns success' do
expect(response).to be_success
expect(response.payload[:alert]).to eq(alert)
end
it 'updates the status' do
expect { response }.to change { alert.acknowledged? }.to(true)
end
context 'resolving status' do
let(:new_status) { AlertManagement::Alert::STATUSES[:resolved] }
it 'updates the status' do
expect { response }.to change { alert.resolved? }.to(true)
end
context 'user has a pending todo' do
let(:user) { create(:user) }
let!(:todo) { create(:todo, :pending, target: alert, user: user, project: alert.project) }
it 'resolves the todo' do
expect { response }.to change { todo.reload.state }.from('pending').to('done')
end
end
end
context 'when user has no permissions' do
let(:can_update) { false }
include_examples 'update failure', _('You have no permissions')
end
context 'with no status' do
let(:new_status) { nil }
include_examples 'update failure', _('Invalid status')
end
context 'with unknown status' do
let(:new_status) { -1 }
include_examples 'update failure', _('Invalid status')
end
end
end

View file

@ -54,29 +54,5 @@ RSpec.describe IncidentManagement::CreateIncidentLabelService do
context 'without label' do
it_behaves_like 'new label'
end
context 'with duplicate labels', issue: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/65042' do
before do
# Replicate race condition to create duplicates
build(:label, project: project, title: title).save!(validate: false)
build(:label, project: project, title: title).save!(validate: false)
end
it 'create an issue without labels' do
# Verify we have duplicates
expect(project.labels.size).to eq(2)
expect(project.labels.map(&:title)).to all(eq(title))
message = <<~MESSAGE.chomp
Cannot create incident label "incident" \
for "#{project.full_name}": Title has already been taken.
MESSAGE
expect(service).to receive(:log_info).with(message)
expect(execute).to be_error
expect(execute.payload[:label]).to be_kind_of(Label)
expect(execute.message).to eq('Title has already been taken')
end
end
end
end

View file

@ -681,4 +681,16 @@ RSpec.describe SystemNoteService do
described_class.unapprove_mr(noteable, author)
end
end
describe '.change_alert_status' do
let(:alert) { build(:alert_management_alert) }
it 'calls AlertManagementService' do
expect_next_instance_of(SystemNotes::AlertManagementService) do |service|
expect(service).to receive(:change_alert_status).with(alert)
end
described_class.change_alert_status(alert, author)
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::SystemNotes::AlertManagementService do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:noteable) { create(:alert_management_alert, :acknowledged, project: project) }
describe '#change_alert_status' do
subject { described_class.new(noteable: noteable, project: project, author: author).change_alert_status(noteable) }
it_behaves_like 'a system note' do
let(:action) { 'status' }
end
it 'has the appropriate message' do
expect(subject.note).to eq("changed the status to **Acknowledged**")
end
end
end