Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-09-20 15:13:23 +00:00
parent 1e62780b8c
commit 06b63cf514
86 changed files with 1020 additions and 489 deletions

View File

@ -93,7 +93,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'app/workers/delete_user_worker.rb'
- 'app/workers/loose_foreign_keys/cleanup_worker.rb'
- 'app/workers/members/expiring_worker.rb'
- 'app/workers/merge_requests/ensure_prepared_worker.rb'
- 'app/workers/packages/debian/cleanup_dangling_package_files_worker.rb'
- 'app/workers/projects/refresh_build_artifacts_size_statistics_worker.rb'
- 'app/workers/prune_old_events_worker.rb'

View File

@ -3503,7 +3503,6 @@ RSpec/NamedSubject:
- 'spec/workers/merge_request_mergeability_check_worker_spec.rb'
- 'spec/workers/merge_requests/close_issue_worker_spec.rb'
- 'spec/workers/merge_requests/create_pipeline_worker_spec.rb'
- 'spec/workers/merge_requests/ensure_prepared_worker_spec.rb'
- 'spec/workers/merge_requests/mergeability_check_batch_worker_spec.rb'
- 'spec/workers/merge_requests/update_head_pipeline_worker_spec.rb'
- 'spec/workers/merge_worker_spec.rb'

View File

@ -275,7 +275,7 @@ gem 'rack', '~> 2.2.9' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'rack-timeout', '~> 0.7.0', require: 'rack/timeout/base' # rubocop:todo Gemfile/MissingFeatureCategory
group :puma do
gem 'puma', '= 6.4.3', require: false, feature_category: :shared
gem 'puma', '= 6.4.0', require: false, feature_category: :shared
gem 'sd_notify', '~> 0.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
end

View File

@ -525,8 +525,8 @@
{"name":"pry-rails","version":"0.3.9","platform":"ruby","checksum":"468662575abb6b67f4a9831219f99290d5eae7bf186e64dd810d0a3e4a8cc4b1"},
{"name":"pry-shell","version":"0.6.4","platform":"ruby","checksum":"ad024882d29912b071a7de65ebea538b242d2dc1498c60c7c2352ef94769f208"},
{"name":"public_suffix","version":"6.0.1","platform":"ruby","checksum":"61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f"},
{"name":"puma","version":"6.4.3","platform":"java","checksum":"373fcfacacaafd0f5a24db18cb99b3f2decb5c5316470169852559aa80adc8ab"},
{"name":"puma","version":"6.4.3","platform":"ruby","checksum":"24a4645c006811d83f2480057d1f54a96e7627b6b90e1c99b260b9dc630eb43e"},
{"name":"puma","version":"6.4.0","platform":"java","checksum":"eb27679e9e665882bab85dfa84704b0615b4f77cec46de014f05b90a5ab36cfe"},
{"name":"puma","version":"6.4.0","platform":"ruby","checksum":"d5dda11362744df9f4694708a62e3cfddf72eba7498c16016ebbb30f106712f9"},
{"name":"pyu-ruby-sasl","version":"0.0.3.3","platform":"ruby","checksum":"5683a6bc5738db5a1bf5ceddeaf545405fb241b4184dd4f2587e679a7e9497e5"},
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
{"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},

View File

@ -1439,7 +1439,7 @@ GEM
tty-markdown
tty-prompt
public_suffix (6.0.1)
puma (6.4.3)
puma (6.4.0)
nio4r (~> 2.0)
pyu-ruby-sasl (0.0.3.3)
raabro (1.4.0)
@ -2220,7 +2220,7 @@ DEPENDENCIES
pry-byebug
pry-rails (~> 0.3.9)
pry-shell (~> 0.6.4)
puma (= 6.4.3)
puma (= 6.4.0)
rack (~> 2.2.9)
rack-attack (~> 6.7.0)
rack-cors (~> 2.0.1)

View File

@ -535,8 +535,8 @@
{"name":"psych","version":"5.1.2","platform":"java","checksum":"1dd68dc609eddbc884e6892e11da942e16f7256bd30ebde9d35449d43043a6fe"},
{"name":"psych","version":"5.1.2","platform":"ruby","checksum":"337322f58fc2bf24827d2b9bd5ab595f6a72971867d151bb39980060ea40a368"},
{"name":"public_suffix","version":"6.0.1","platform":"ruby","checksum":"61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f"},
{"name":"puma","version":"6.4.3","platform":"java","checksum":"373fcfacacaafd0f5a24db18cb99b3f2decb5c5316470169852559aa80adc8ab"},
{"name":"puma","version":"6.4.3","platform":"ruby","checksum":"24a4645c006811d83f2480057d1f54a96e7627b6b90e1c99b260b9dc630eb43e"},
{"name":"puma","version":"6.4.0","platform":"java","checksum":"eb27679e9e665882bab85dfa84704b0615b4f77cec46de014f05b90a5ab36cfe"},
{"name":"puma","version":"6.4.0","platform":"ruby","checksum":"d5dda11362744df9f4694708a62e3cfddf72eba7498c16016ebbb30f106712f9"},
{"name":"pyu-ruby-sasl","version":"0.0.3.3","platform":"ruby","checksum":"5683a6bc5738db5a1bf5ceddeaf545405fb241b4184dd4f2587e679a7e9497e5"},
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
{"name":"racc","version":"1.6.2","platform":"java","checksum":"0880781e7dfde09e665d0b6160b583e01ed52fcc2955d7891447d33c2d1d2cf1"},

View File

@ -1456,7 +1456,7 @@ GEM
psych (5.1.2)
stringio
public_suffix (6.0.1)
puma (6.4.3)
puma (6.4.0)
nio4r (~> 2.0)
pyu-ruby-sasl (0.0.3.3)
raabro (1.4.0)
@ -2246,7 +2246,7 @@ DEPENDENCIES
pry-byebug
pry-rails (~> 0.3.9)
pry-shell (~> 0.6.4)
puma (= 6.4.3)
puma (= 6.4.0)
rack (~> 2.2.9)
rack-attack (~> 6.7.0)
rack-cors (~> 2.0.1)

View File

@ -39,7 +39,7 @@ export default {
<span v-if="mergeRequest">
{{ __('in') }}
<gl-link :href="mergeRequest.path" class="!gl-text-blue-500" data-testid="link-commit"
<gl-link :href="mergeRequest.path" class="!gl-text-link" data-testid="link-commit"
>!{{ mergeRequest.iid }}</gl-link
>
</span>

View File

@ -23,7 +23,7 @@ export default {
:href="externalLink.url"
target="_blank"
rel="noopener noreferrer nofollow"
class="!gl-text-blue-600"
class="!gl-text-link"
>
<gl-icon name="external-link" class="flex-shrink-0" />
{{ externalLink.label }}

View File

@ -41,12 +41,7 @@ export default {
<template>
<p class="build-sidebar-item gl-mb-3 gl-flex gl-leading-normal">
<b v-if="hasTitle" class="gl-mr-3">{{ title }}:</b>
<gl-link
v-if="path"
:href="path"
class="!gl-text-blue-600"
data-testid="job-sidebar-value-link"
>
<gl-link v-if="path" :href="path" class="!gl-text-link" data-testid="job-sidebar-value-link">
{{ value }}
</gl-link>
<span v-else

View File

@ -102,7 +102,7 @@ export default {
<template #id>
<gl-link
:href="pipeline.path"
class="js-pipeline-path link-commit !gl-text-blue-500"
class="js-pipeline-path link-commit !gl-text-link"
data-testid="pipeline-path"
>#{{ pipeline.id }}</gl-link
>
@ -117,7 +117,7 @@ export default {
<template #mrId>
<gl-link
:href="pipeline.merge_request.path"
class="link-commit !gl-text-blue-500"
class="link-commit !gl-text-link"
data-testid="mr-link"
>!{{ pipeline.merge_request.iid }}</gl-link
>

View File

@ -255,10 +255,7 @@ export default {
{{ downstreamTitle }}
</span>
<div class="-gl-m-2 gl-truncate gl-p-2">
<gl-link
class="gl-text-sm !gl-text-blue-500"
:href="pipeline.path"
data-testid="pipelineLink"
<gl-link class="gl-text-sm" :href="pipeline.path" data-testid="pipelineLink"
>#{{ pipeline.id }}</gl-link
>
</div>

View File

@ -50,7 +50,7 @@ export default {
class="js-run-pipeline"
data-testid="run-pipeline-button"
>
{{ s__('Pipeline|Run pipeline') }}
{{ s__('Pipeline|New pipeline') }}
</gl-button>
</div>
</template>

View File

@ -136,12 +136,9 @@ export default {
<div v-if="pipelineName" class="gl-mb-2" data-testid="pipeline-name-container">
<span class="gl-flex">
<tooltip-on-truncate :title="pipelineName" class="gl-grow gl-truncate gl-text-gray-900">
<gl-link
:href="pipeline.path"
class="!gl-text-blue-600"
data-testid="pipeline-url-link"
>{{ pipelineName }}</gl-link
>
<gl-link :href="pipeline.path" class="!gl-text-link" data-testid="pipeline-url-link">{{
pipelineName
}}</gl-link>
</tooltip-on-truncate>
</span>
</div>
@ -168,7 +165,7 @@ export default {
<div class="gl-mb-2">
<gl-link
:href="pipeline.path"
class="gl-mr-1 !gl-text-blue-500"
class="gl-mr-1 !gl-text-link"
data-testid="pipeline-url-link"
@click="trackClick('click_pipeline_id')"
>#{{ pipeline[pipelineIdType] }}</gl-link

View File

@ -201,7 +201,7 @@ export default {
<gl-link
v-gl-tooltip
:href="environment.environmentPath"
class="gl-truncate gl-text-blue-500"
class="gl-truncate"
:class="{ 'gl-font-bold': visible }"
:title="name"
>

View File

@ -210,7 +210,7 @@ export default {
<gl-link
:title="getNoteableData.target_branch"
:href="getNoteableData.target_branch_path"
class="gl-ml-2 gl-mt-2 gl-max-w-26 gl-truncate gl-rounded-base gl-bg-blue-50 gl-px-2 gl-text-sm !gl-text-blue-500 gl-font-monospace"
class="gl-ml-2 gl-mt-2 gl-max-w-26 gl-truncate gl-rounded-base gl-bg-blue-50 gl-px-2 gl-text-sm !gl-text-link gl-font-monospace"
>
{{ getNoteableData.target_branch }}
</gl-link>

View File

@ -57,7 +57,7 @@ export default {
v-gl-tooltip
:href="helpPath"
:title="$options.i18n.helpLabel"
class="gl-leading-1 gl-text-blue-600"
class="gl-leading-1"
target="_blank"
>
<gl-icon name="question-o" />

View File

@ -184,7 +184,7 @@ export default {
<template v-else>
<ul class="gl-mb-0 gl-list-none gl-p-0">
<li v-for="(issuable, index) in displayedIssuables" :key="issuable.id">
<gl-link :href="issuable.webUrl" class="gl-text-sm !gl-text-blue-500">{{
<gl-link :href="issuable.webUrl" class="gl-text-sm !gl-text-link">{{
issuable.reference
}}</gl-link>
<p
@ -204,7 +204,7 @@ export default {
<gl-link
data-testid="view-all-issues"
:href="`${item.webUrl}#related-issues`"
class="gl-text-sm !gl-text-blue-500"
class="gl-text-sm !gl-text-link"
>{{ viewAllIssuablesText }}</gl-link
>
</div>

View File

@ -218,7 +218,7 @@ export default {
></div>
<div v-if="hasMoreCommits" class="flex-list">
<div
class="flex-row gl-relative gl-z-2 gl-cursor-pointer gl-pl-4 gl-pt-3 gl-text-blue-500 hover:gl-underline"
class="flex-row gl-relative gl-z-2 gl-cursor-pointer gl-pl-4 gl-pt-3 gl-text-link hover:gl-underline"
@click="expanded = !expanded"
>
<gl-icon :name="toggleIcon" :size="12" class="gl-mr-2" />

View File

@ -33,7 +33,7 @@ export default {
: __('Collapse replies');
},
toggleTextColor() {
return this.collapsed ? 'gl-text-blue-500' : 'gl-text-gray-900';
return this.collapsed ? 'gl-text-link' : 'gl-text-default';
},
authors() {
return [...new Set(this.replies.map((item) => item.author))];

View File

@ -332,7 +332,7 @@ export default {
}}
</p>
<details class="gl-mb-5">
<summary class="gl-text-blue-500">{{ s__('WorkItem|View current version') }}</summary>
<summary class="gl-text-link">{{ s__('WorkItem|View current version') }}</summary>
<gl-form-textarea
class="js-gfm-input js-autosize markdown-area !gl-font-monospace"
data-testid="conflicted-description"

View File

@ -449,12 +449,12 @@ li.note {
}
.ref-container {
color: var(--blue-500, $blue-500) !important;
@apply gl-text-link #{!important};
background-color: var(--blue-50, $blue-50);
}
.commit-sha-container {
color: var(--gray-700, $gray-700) !important;
@apply gl-text-subtle #{!important};
background-color: var(--gray-50, $gray-50);
}

View File

@ -828,7 +828,6 @@ h4 {
background: $line-added-dark;
}
/**
* form text input i.e. search bar, comments, forms, etc.
*/

View File

@ -85,8 +85,8 @@ $gl-text-color-tertiary: $gray-400;
$gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85);
$gl-text-color-disabled: $gray-400;
$link-color: $blue-500 !default;
$link-hover-color: $blue-500 !default;
$link-color: var(--gl-text-color-link) !default;
$link-hover-color: var(--gl-text-color-link) !default;
$gl-grayish-blue: #7f8fa4;
$gl-font-size-12: 12px;
$gl-font-size-14: 14px;

View File

@ -105,14 +105,3 @@ $type-scale: (
.gl-last-of-type-border-b-0:last-of-type {
border-bottom-width: 0;
}
// stylelint-disable-next-line gitlab/no-gl-class
.gl-pl-12 {
padding-left: $gl-spacing-scale-12;
}
// stylelint-disable-next-line gitlab/no-gl-class
.gl-min-w-12 {
min-width: $gl-spacing-scale-12;
}

View File

@ -12,7 +12,7 @@ module StatAnchorsHelper
private
def button_attribute(anchor)
anchor.class_modifier || 'btn-link gl-button !gl-text-blue-500'
anchor.class_modifier || 'btn-link gl-button !gl-text-link'
end
def extra_classes(anchor)

View File

@ -80,7 +80,7 @@ module Ci
end
def tag_list_cache_set?
strong_memoized?(:tag_list) && tag_list.any?
strong_memoized?(:tag_list)
end
def save_tags

View File

@ -1,15 +0,0 @@
# frozen_string_literal: true
module Onboarding
class ProgressService
def initialize(namespace)
@namespace = namespace&.root_ancestor
end
def execute(action:)
return unless @namespace
Onboarding::Progress.register(@namespace, action)
end
end
end

View File

@ -38,7 +38,7 @@
.card-header
= _('Quick help')
%ul.content-list
%li= link_to _('See our website for help'), support_url, { class: '!gl-text-blue-600' }
%li= link_to _('See our website for help'), support_url, { class: '!gl-text-link' }
%li
%button.btn-blank.btn-link.gl-button.js-trigger-search-bar{ type: 'button' }
= _('Use the search bar on the top of this page')
@ -46,5 +46,5 @@
%button.btn-blank.btn-link.gl-button.js-trigger-shortcut{ type: 'button' }
= _('Use shortcuts')
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
%li= link_to _('Get a support subscription'), "#{ApplicationHelper.promo_url}/pricing/", { class: '!gl-text-blue-600' }
%li= link_to _('Compare GitLab editions'), "#{ApplicationHelper.promo_url}/features/#compare", { class: '!gl-text-blue-600' }
%li= link_to _('Get a support subscription'), "#{ApplicationHelper.promo_url}/pricing/", { class: '!gl-text-link' }
%li= link_to _('Compare GitLab editions'), "#{ApplicationHelper.promo_url}/features/#compare", { class: '!gl-text-link' }

View File

@ -1,6 +1,6 @@
- page_title _('Emails')
- profile_message = _('Used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{user_settings_profile_path}' class='!gl-text-blue-500'>".html_safe, closingTag: '</a>'.html_safe}
- notification_message = _('Used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{profile_notifications_path}' class='!gl-text-blue-500'>".html_safe, closingTag: '</a>'.html_safe}
- profile_message = _('Used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{user_settings_profile_path}' class='!gl-text-link'>".html_safe, closingTag: '</a>'.html_safe}
- notification_message = _('Used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{profile_notifications_path}' class='!gl-text-link'>".html_safe, closingTag: '</a>'.html_safe}
- public_email_message = _('Your public email will be displayed on your public profile.')
- commit_email_message = _('Used for web based operations, such as edits and merges.')
- @force_desktop_expanded_sidebar = true

View File

@ -18,9 +18,9 @@
%td
- if can?(current_user, :read_build, job)
= link_to job.name, project_job_path(job.project, job), class: '!gl-text-blue-600'
= link_to job.name, project_job_path(job.project, job), class: '!gl-text-link'
- else
%span{ class: '!gl-text-blue-600' }
%span{ class: '!gl-text-link' }
= job.name
%td.branch-commit.gl-text-gray-900

View File

@ -2,4 +2,4 @@
%span
= sprite_icon("rocket", size: 12, css_class: "gl-text-secondary")
= _("Release:")
= link_to release.name, project_release_path(project, release), class: "!gl-text-blue-600"
= link_to release.name, project_release_path(project, release), class: "!gl-text-link"

View File

@ -8,15 +8,7 @@
- tracking_data = { event_tracking: 'click_new_merge_request_empty_list' }
- if has_filter_bar_param?
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-search-md.svg',
empty_state_options: { data: { testid: 'issuable-empty-state' } },
title: _("Sorry, your filter produced no results")) do |c|
- c.with_description do
= _("To widen your search, change or remove filters above")
- if can_create_merge_request
.gl-mt-5= link_button_to button_text, button_path || project_new_merge_request_path(@project),
title: button_text, variant: :confirm, data: tracking_data
= render ::Layouts::EmptyResultComponent.new(type: :filter, data: { testid: 'issuable-empty-state' })
- elsif is_opened_state && opened_merged_count == 0 && closed_merged_count > 0
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-merge-requests-md.svg',

View File

@ -624,15 +624,6 @@
:weight: 1
:idempotent: false
:tags: []
- :name: cronjob:merge_requests_ensure_prepared
:worker_name: MergeRequests::EnsurePreparedWorker
:feature_category: :code_review_workflow
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:metrics_global_metrics_update
:worker_name: Metrics::GlobalMetricsUpdateWorker
:feature_category: :metrics

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
module MergeRequests
class EnsurePreparedWorker
include ApplicationWorker
include CronjobQueue
feature_category :code_review_workflow
idempotent!
deduplicate :until_executed
data_consistency :sticky
JOBS_PER_10_SECONDS = 5
def perform
return unless Feature.enabled?(:ensure_merge_requests_prepared)
scope = MergeRequest.recently_unprepared
iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)
index = 0
iterator.each_batch(of: JOBS_PER_10_SECONDS) do |merge_requests|
index += 1
NewMergeRequestWorker.bulk_perform_in_with_contexts(index * 10.seconds,
merge_requests,
arguments_proc: ->(merge_request) { [merge_request.id, merge_request.author_id] },
context_proc: ->(merge_request) { { project: merge_request.project } }
)
end
end
end
end

View File

@ -1,8 +0,0 @@
---
name: ensure_merge_requests_prepared
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121999
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/413884
milestone: '16.4'
type: development
group: group::code review
default_enabled: false

View File

@ -25,7 +25,10 @@
"enum": [
"user.id",
"project.id",
"namespace.id"
"namespace.id",
"label",
"property",
"value"
]
},
"filter": {

View File

@ -749,6 +749,8 @@
- 1
- - search_zoekt_indexing_task
- 1
- - search_zoekt_initial_indexing_event
- 1
- - search_zoekt_namespace_indexer
- 1
- - search_zoekt_namespace_initial_indexing

View File

@ -88,7 +88,7 @@ Note the following when promoting a secondary:
the **secondary** to the **primary**.
- If you encounter an `ActiveRecord::RecordInvalid: Validation failed: Name has already been taken`
error message during this process, for more information, see this
[troubleshooting advice](failover_troubleshooting.md).md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-site).
[troubleshooting advice](failover_troubleshooting.md#fixing-errors-during-a-failover-or-when-promoting-a-secondary-to-a-primary-site).
- If you are using separate URLs, you should [point the primary domain DNS at the newly promoted site](#step-4-optional-updating-the-primary-domain-dns-record). Otherwise, runners must be registered again with the newly promoted site, and all Git remotes, bookmarks, and external integrations must be updated.
- If you are using [location-aware DNS](../secondary_proxy/index.md#configure-location-aware-dns), the runners should automatically connect to the new primary after the old primary is removed from the DNS entry.
- If you don't expect the runners connected to the previous primary to come back, you should remove them:

View File

@ -224,7 +224,7 @@ Use the `CI_PIPELINE_SOURCE` variable to control when to add jobs for these pipe
| `schedule` | For [scheduled pipelines](../pipelines/schedules.md). |
| `security_orchestration_policy` | For [security orchestration policy](../../user/application_security/policies/index.md) pipelines. |
| `trigger` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
| `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
| `web` | For pipelines created by selecting **New pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
| `webide` | For pipelines created by using the [WebIDE](../../user/project/web_ide/index.md). |
These values are the same as returned for the `source` parameter when using the

View File

@ -91,7 +91,7 @@ To execute a pipeline manually:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Build > Pipelines**.
1. Select **Run pipeline**.
1. Select **New pipeline**.
1. In the **Run for branch name or tag** field, select the branch or tag to run the pipeline for.
1. Enter any [CI/CD variables](../variables/index.md) required for the pipeline to run.
You can set specific variables to have their [values prefilled in the form](#prefill-variables-in-manual-pipelines).
@ -108,7 +108,7 @@ information such as what the variable is used for, and what the acceptable value
Job-level variables cannot be pre-filled.
In manually-triggered pipelines, the **Run pipeline** page displays all pipeline-level variables
In manually-triggered pipelines, the **New pipeline** page displays all pipeline-level variables
that have a `description` defined in the `.gitlab-ci.yml` file. The description displays
below the variable.
@ -131,9 +131,9 @@ variables:
In this example:
- `DEPLOY_CREDENTIALS` is listed in the **Run pipeline** page, but with no value set.
- `DEPLOY_CREDENTIALS` is listed in the **New pipeline** page, but with no value set.
The user is expected to define the value each time the pipeline is run manually.
- `DEPLOY_ENVIRONMENT` is pre-filled in the **Run pipeline** page with `canary` as the default value,
- `DEPLOY_ENVIRONMENT` is pre-filled in the **New pipeline** page with `canary` as the default value,
and the message explains the other options.
NOTE:
@ -149,7 +149,7 @@ when running a pipeline manually. To workaround this issue,
> - The variables list sometimes did not populate correctly due to [a bug](https://gitlab.com/gitlab-org/gitlab/-/issues/386245), which was resolved in GitLab 15.9.
You can define an array of CI/CD variable values the user can select from when running a pipeline manually.
These values are in a dropdown list in the **Run pipeline** page. Add the list of
These values are in a dropdown list in the **New pipeline** page. Add the list of
value options to `options` and set the default value with `value`. The string in `value`
must also be included in the `options` list.
@ -168,9 +168,9 @@ variables:
### Run a pipeline by using a URL query string
You can use a query string to pre-populate the **Run Pipeline** page. For example, the query string
You can use a query string to pre-populate the **New pipeline** page. For example, the query string
`.../pipelines/new?ref=my_branch&var[foo]=bar&file_var[file_foo]=file_bar` pre-populates the
**Run Pipeline** page with:
**New pipeline** page with:
- **Run for** field: `my_branch`.
- **Variables** section:

View File

@ -2875,7 +2875,7 @@ Use `interruptible` to configure the [auto-cancel redundant pipelines](../pipeli
feature to cancel a job before it completes if a new pipeline on the same ref starts for a newer commit. If the feature
is disabled, the keyword has no effect. The new pipeline must be for a commit with new changes. For example,
the **Auto-cancel redundant pipelines** feature has no effect
if you select **Run pipeline** in the UI to run a pipeline for the same commit.
if you select **New pipeline** in the UI to run a pipeline for the same commit.
The behavior of the **Auto-cancel redundant pipelines** feature can be controlled by
the [`workflow:auto_cancel:on_new_commit`](#workflowauto_cancelon_new_commit) setting.
@ -5841,7 +5841,7 @@ pipeline based on branch names or pipeline types.
| `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
| `tags` | When the Git reference for a pipeline is a tag. |
| `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#configure-cicd-jobs-to-run-in-triggered-pipelines). |
| `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
| `web` | For pipelines created by selecting **New pipeline** in the GitLab UI, from the project's **Build > Pipelines** section. |
**Example of `only:refs` and `except:refs`**:

View File

@ -205,6 +205,12 @@ See [Recommended process for adding a new document type](#recommended-process-fo
### Create the index
NOTE
All new indexes must have:
- `project_id` and `namespace_id` fields (if available). One of the fields must be used for routing.
- A `traversal_ids` field for efficient global and group search. Populate the field with `object.namespace.elastic_namespace_ancestry`
1. Create a `Search::Elastic::Types::` class in `ee/lib/search/elastic/types/`.
1. Define the following class methods:
- `index_name`: in the format `gitlab-<env>-<type>` (for example, `gitlab-production-work_items`).
@ -336,6 +342,15 @@ Always check for `Gitlab::CurrentSettings.elasticsearch_indexing?` and `use_elas
Also check that the index is able to handle the index request. For example, check that the index exists if it was added in the current major release by verifying that the migration to add the index was completed: `Elastic::DataMigrationService.migration_has_finished?`.
#### Transfers and deletes
Project and group transfers and deletes must make updates to the index to avoid orphaned data.
Indexes that contain a `project_id` field must use the [`Search::Elastic::DeleteWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/search/elastic/delete_worker.rb). Indexes that contain a `namespace_id` field but no `project_id` field must use [`Search::ElasticGroupAssociationDeleteWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/search/elastic_group_association_deletion_worker.rb).
1. Add the indexed class to `excluded_classes` in [`ElasticDeleteProjectWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/workers/elastic_delete_project_worker.rb)
1. Update the worker to remove documents from the index
### Recommended process for adding a new document type
Create the following MRs and have them reviewed by a member of the Global Search team:
@ -672,7 +687,7 @@ Requires `source_branch` field. Query with `source_branch` or `not_source_branch
##### `by_search_level_and_membership`
Requires `project_id` or `traversal_id` fields. Supports feature `*_access_level` fields. Query with `search_level`
Requires `project_id` and `traversal_id` fields. Supports feature `*_access_level` fields. Query with `search_level`
and optionally `project_ids`, `group_ids`, `features`, and `current_user` in options.
Filtering is applied for:

View File

@ -29,6 +29,8 @@ You can follow this tutorial to familiarize yourself with the contribution proce
1. We recommend you join the [GitLab Discord server](https://discord.com/invite/gitlab), where GitLab team
members and the wider community are ready and waiting to answer your questions and offer support
for making contributions.
1. Visit the [Duo access project](https://gitlab.com/gitlab-community/community-members/duo-access)
to request a GitLab Duo Pro license and learn more about the benefits of Code Suggestions, Chat and more AI-powered features with [GitLab Duo](https://about.gitlab.com/gitlab-duo/).
## Choose how you want to contribute

View File

@ -32,7 +32,7 @@ A snippet from a unique metric could look like below. Notice the `unique` proper
```yaml
events:
- name: create_merge_request
unique: user_id
unique: user.id
```
Similarly, a snippet from a total count metric can look like below. Notice how there is no `unique` property.
@ -136,7 +136,7 @@ Each internal event based metric should have a least one event selection rule wi
| Property | Required | Additional information |
|--------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | yes | Name of the event |
| `unique` | no | Used if the metric should count the distinct number of users, projects or namespaces present in the event. Valid values are `user.id`, `project.id` and `namespace.id`. |
| `unique` | no | Used if the metric should count the distinct number of users, projects, namespaces, or count the unique values for additional properties present in the event. Valid values are `user.id`, `project.id` and `namespace.id`. Additionally `label`, `property`, and `value` may also be used in reference to any [additional properties](quick_start.md#additional-properties) included with the event. |
| `filter` | no | Used when only a subset of events should be included in the metric. Only additional properties can be used for filtering. |
An example of a single event selection rule which updates a unique count metric when an event called `pull_package` with additional property `label` with the value `rubygems` occurs:

View File

@ -87,7 +87,7 @@ the necessary CI/CD variables to deploy the Status Page to AWS S3:
- `AWS_ACCESS_KEY_ID` - The AWS access key ID.
- `AWS_SECRET_ACCESS_KEY` - The AWS secret.
1. On the left sidebar, select **Build > Pipelines**.
1. To deploy the Status Page to S3, select **Run pipeline**.
1. To deploy the Status Page to S3, select **New pipeline**.
WARNING:
Consider limiting who can access issues in this project, as any user who can view

View File

@ -145,7 +145,7 @@ Auto DevOps pipeline for any project that belongs to that group:
1. On the left sidebar, select **Search or go to** and find your project.
1. Make sure the project doesn't contain a `.gitlab-ci.yml` file.
1. Select **Build > Pipelines**.
1. To trigger the Auto DevOps pipeline, select **Run pipeline**.
1. To trigger the Auto DevOps pipeline, select **New pipeline**.
#### Per instance

View File

@ -121,8 +121,8 @@ To run the compliance pipeline configuration in `Tutorial project`:
1. On the left sidebar, select **Search or go to** and find the `Tutorial project` project.
1. Select **Build > Pipelines**.
1. Select **Run pipeline**.
1. On the **Run pipeline** page, select **Run pipeline**.
1. Select **New pipeline**.
1. On the **New pipeline** page, select **Run pipeline**.
Notice the pipeline runs a job called `compliance-job` in a **test** stage. Nice work, you've run your first compliance
job!
@ -169,8 +169,8 @@ To confirm the regular pipeline configuration is combined with the compliance pi
1. On the left sidebar, select **Search or go to** and find the `Tutorial project` project.
1. Select **Build > Pipelines**.
1. Select **Run pipeline**.
1. On the **Run pipeline** page, select **Run pipeline**.
1. Select **New pipeline**.
1. On the **New pipeline** page, select **Run pipeline**.
Notice the pipeline runs two jobs in a **test** stage:

View File

@ -132,7 +132,7 @@ Next, trigger a pipeline in your project so you can view your runner execute a j
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Build > Pipelines**.
1. Select **Run pipeline**.
1. Select **New pipeline**.
1. Select a job to view the job log. The output should look similar to this example, which shows
your runner successfully executing the job:

View File

@ -91,8 +91,8 @@ Refer to the [Civo Terraform provider](https://registry.terraform.io/providers/c
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
1. On the left sidebar, go to **Build > Pipelines**.
1. On the left sidebar, select **Build > Pipelines**.
1. Select **New pipeline**.
1. Select **Run pipeline**, and then select the newly created pipeline from the list.
1. Next to the **deploy** job, select **Manual action** (**{status_manual}**).

View File

@ -134,7 +134,7 @@ From the Google Cloud console, enable the [Kubernetes Engine API](https://consol
After configuring your project, manually trigger the provisioning of your cluster. In GitLab:
1. On the left sidebar, select **Build > Pipelines**.
1. Select **Run pipeline**.
1. Select **New pipeline**.
1. Next to **Play** (**{play}**), select the dropdown list icon (**{chevron-lg-down}**).
1. Select **Deploy** to manually trigger the deployment job.

View File

@ -89,7 +89,7 @@ In your Auto DevOps project, you can use the GitLab agent to connect with your K
KUBE_NAMESPACE: "demo-agent"
```
1. To test your pipeline, on the left sidebar, select **Build > Pipelines** and then **Run pipeline**.
1. To test your pipeline, on the left sidebar, select **Build > Pipelines** and then **New pipeline**.
For an example, [view this project](https://gitlab.com/gitlab-examples/ops/gitops-demo/hello-world-service).

View File

@ -23,7 +23,7 @@ To fork a sample project and create a Pages website:
1. View the sample projects by navigating to the [GitLab Pages examples](https://gitlab.com/pages) group.
1. Select the name of the project you want to [fork](../../repository/forking_workflow.md#create-a-fork).
1. In the upper-right corner, select **Fork**, then choose a namespace to fork to.
1. For your project, on the left sidebar, select **Build > Pipelines** and then **Run pipeline**.
1. For your project, on the left sidebar, select **Build > Pipelines** and then **New pipeline**.
GitLab CI/CD builds and deploys your site.
The site can take approximately 30 minutes to deploy.

View File

@ -24,7 +24,7 @@ configured to generate a Pages site.
1. Complete the form and select **Create project**.
1. On the left sidebar, select **Build > Pipelines**
and select **Run pipeline** to trigger GitLab CI/CD to build and deploy your
and select **New pipeline** to trigger GitLab CI/CD to build and deploy your
site.
When the pipeline is finished, go to **Deploy > Pages** to find the link to

View File

@ -94,7 +94,7 @@ module Gitlab
if event_selection_rule.total_counter?
update_total_counter(event_selection_rule)
else
update_unique_counter(event_selection_rule, kwargs)
update_unique_counter(event_selection_rule, **kwargs, **additional_properties)
end
end
end
@ -110,16 +110,17 @@ module Gitlab
increment(event_selection_rule.redis_key_for_date, expiry: expiry)
end
def update_unique_counter(event_selection_rule, kwargs)
def update_unique_counter(event_selection_rule, properties)
identifier_name = event_selection_rule.unique_identifier_name
unless kwargs[identifier_name]
unless properties[identifier_name]
message = "#{event_selection_rule.name} should be triggered with a named parameter '#{identifier_name}'."
Gitlab::AppJsonLogger.warn(message: message)
return
end
unique_value = kwargs[identifier_name].id
# Use id for ActiveRecord objects, else normalize size of stored value
unique_value = properties[identifier_name].try(:id) || properties[identifier_name].hash
# Overrides for legacy keys of unique counters are handled in `event_selection_rule.redis_key_for_date`
Gitlab::Redis::HLL.add(

View File

@ -95,18 +95,16 @@ namespace :gitlab do
end
end
def add_sent_notification_shas(project, refs)
logger.info "Checking sent notification shas..."
notifications = SentNotification.where(project: project).where.not(commit_id: nil).select(:id, :commit_id)
notifications.find_each do |notification|
add_match(refs, notification.commit_id)
end
def add_sent_notification_shas(_project, _refs)
logger.warn "Sent notifications will not be included."
end
def add_todo_shas(project, refs)
logger.info "Checking todo shas..."
Todo.where(project: project).where.not(commit_id: nil).select(:id, :commit_id).find_each do |todo|
add_match(refs, todo.commit_id)
Todo.where(project: project).each_batch(of: 1000) do |b|
b.where.not(commit_id: nil).select(:commit_id).each do |todo|
add_match(refs, todo.commit_id)
end
end
end

View File

@ -40531,6 +40531,9 @@ msgstr ""
msgid "Pipeline|Merged result pipeline runs on the contents of the merge request combined with the contents of the target branch."
msgstr ""
msgid "Pipeline|New pipeline"
msgstr ""
msgid "Pipeline|Only the first 100 jobs per stage are displayed"
msgstr ""

View File

@ -117,7 +117,7 @@ module InternalEventsCli
available_props = [:label, :property, :value]
while available_props.any?
disabled = format_warning('(already defined)')
disabled = format_help('(already defined)')
# rubocop:disable Rails/NegateInclude -- this isn't Rails
options = [
@ -144,7 +144,8 @@ module InternalEventsCli
"Which additional property do you want to add to the event?",
options,
help: format_help("(will reprompt for multiple)"),
**select_opts
**select_opts,
&disabled_format_callback
)
if selected_property == :none

View File

@ -87,6 +87,11 @@ module InternalEventsCli
}
end
# For use when menu options are disabled by being grayed out
def disabled_format_callback
proc { |menu| menu.symbols(cross: format_help("")) }
end
# Help text to use with required, multiline cli#ask prompts.
# Otherwise, prefer #prompt_for_text.
def input_required_text

View File

@ -4,13 +4,6 @@
module InternalEventsCli
module Helpers
module MetricOptions
EVENT_PHRASES = {
'user' => "who triggered %s",
'namespace' => "where %s occurred",
'project' => "where %s occurred",
nil => "%s occurrences"
}.freeze
# Creates a list of metrics to be used as options in a
# select/multiselect menu; existing metrics and metrics for
# unavailable identifiers are marked as disabled
@ -21,14 +14,9 @@ module InternalEventsCli
# name: [String] formatted description of the metrics
# disabled: [String] reason metrics are disabled
def get_metric_options(events)
actions = events.map(&:action)
options = get_all_metric_options(actions)
identifiers = get_identifiers_for_events(events)
metric_name = format_metric_name_for_events(events)
filter_name = format_filter_options_for_events(events)
options.reject!(&:filters_expected?) unless filter_name
selection = EventSelection.new(events)
options = get_all_metric_options(selection.actions)
options = options.group_by do |metric|
[
metric.identifier.value,
@ -38,16 +26,27 @@ module InternalEventsCli
]
end
options.map do |(identifier, defined, filtered, _), metrics|
format_metric_option(
identifier,
metric_name,
(filter_name if filtered),
metrics,
options = options.filter_map do |(identifier, defined, filtered, _), metrics|
# Hide the filtered version of an option if unsupported; it just adds noise without value. Still,
# showing unsupported options is valuable, because it advertises possibilities and explains why
# those options aren't available.
next if filtered && !selection.can_be_unique?(identifier)
next if filtered && !selection.can_filter_when_unique?(identifier)
next if selection.exclude_filter_identifier?(identifier)
Option.new(
identifier: identifier,
events_name: selection.events_name,
filter_name: (selection.filter_name(identifier) if filtered),
metrics: metrics,
defined: defined,
supported: [*identifiers, nil].include?(identifier)
)
supported: selection.can_be_unique?(identifier)
).formatted
end
# Push disabled options to the end for better skimability;
# retain relative order for continuity
options.partition { |opt| !opt[:disabled] }.flatten
end
private
@ -61,14 +60,14 @@ module InternalEventsCli
[
Metric.new(actions: actions, time_frame: '28d', identifier: 'user'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'user'),
Metric.new(actions: actions, time_frame: '28d', identifier: 'user', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'user', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'project'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'project'),
Metric.new(actions: actions, time_frame: '28d', identifier: 'project', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'project', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace'),
Metric.new(actions: actions, time_frame: '28d', identifier: 'user', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'user', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'project', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'project', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'namespace', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'namespace', filters: []),
Metric.new(actions: actions, time_frame: '28d'),
@ -76,40 +75,22 @@ module InternalEventsCli
Metric.new(actions: actions, time_frame: '28d', filters: []),
Metric.new(actions: actions, time_frame: '7d', filters: []),
Metric.new(actions: actions, time_frame: 'all'),
Metric.new(actions: actions, time_frame: 'all', filters: [])
Metric.new(actions: actions, time_frame: 'all', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'label'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'label'),
Metric.new(actions: actions, time_frame: '28d', identifier: 'property'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'property'),
Metric.new(actions: actions, time_frame: '28d', identifier: 'value'),
Metric.new(actions: actions, time_frame: '7d', identifier: 'value'),
Metric.new(actions: actions, time_frame: '28d', identifier: 'label', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'label', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'property', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'property', filters: []),
Metric.new(actions: actions, time_frame: '28d', identifier: 'value', filters: []),
Metric.new(actions: actions, time_frame: '7d', identifier: 'value', filters: [])
]
end
# Very brief summary of the provided events to use in a basic
# description of the metric; does not account for filters
#
# @param events [Array<ExistingEvent>]
# @return [String]
def format_metric_name_for_events(events)
return events.first.action if events.length == 1
"any of #{events.length} events"
end
# Formats the list of the additional properties available
# across any of the events
#
# @param events [Array<ExistingEvent>]
# @return [String] ex) "label/property"
def format_filter_options_for_events(events)
available_filters = events.flat_map(&:available_filters).uniq
available_filters.join('/') if available_filters.any?
end
# Get only the identifiers in common for all events
#
# @param events [Array<ExistingEvent>]
# @return [Array<String>]
def get_identifiers_for_events(events)
events.map(&:identifiers).reduce(&:&)
end
# Checks if there's an existing metric which has the same
# properties as the new one
#
@ -127,38 +108,136 @@ module InternalEventsCli
end
end
# Formats & assembles a single select/multiselect menu item,
# Represents the attributes of set of events that depend on
# the other events in the set
EventSelection = Struct.new(:events) do
def actions
events.map(&:action)
end
# Very brief summary of the provided events to use in a
# basic description of the metric
# This ignores filters for simplicity & skimability
def events_name
return actions.first if actions.length == 1
"any of #{actions.length} events"
end
# Formatted list of filter options for these events, given
# the provided uniqueness constraint
def filter_name(identifier)
filter_options.difference([identifier]).join('/')
end
# We accept different filters for each event, so we want
# any filter options available for any event
def filter_options
events.flat_map(&:available_filters).uniq
end
# We require the same uniqueness constraint for all events,
# so we want only the options they have in common
def uniqueness_options
[*shared_identifiers, *shared_filters, nil]
end
# Whether there are any filtering options other than the
# selected uniqueness constraint
def can_filter_when_unique?(identifier)
can_be_unique?(identifier) && filter_options.difference([identifier]).any?
end
# Whether the given identifier is available for all events
# and can be used as a uniqueness constraint
def can_be_unique?(identifier)
uniqueness_options.include?(identifier)
end
# Common values for identifiers shared across all the events
def shared_identifiers
events.map(&:identifiers).reduce(&:&)
end
# Common values for filters shared across all the events
def shared_filters
events.map(&:available_filters).reduce(&:&)
end
# Whether none of the events have additional properties
# and the given identifier is an additional property.
# In this case, it makes sense to exclude these from the
# menu to keep the flow simple when the use-case is simple
def exclude_filter_identifier?(identifier)
return false if identifier.nil? || Metric::Identifier.new(identifier).default?
filter_options.empty?
end
end
# Formats & structures a single select/multiselect menu item
#
# @param identifier [String] user/project/namespace (must support unique metrics)
# @param event_name [String]
# @param filter_name [String]
# @param identifier [String, nil] if present, used in unique-by-identifier metrics
# @param events_name [String] how the selected events will be referred to as a group
# @param filter_name [String] how the potential filters will be referred to as a group
# @param metrics [Array<NewMetric>]
# @option defined [Boolean]
# @option supported [Boolean]
# @return [Hash] see #get_metric_options for format
def format_metric_option(identifier, event_name, filter_name, metrics, defined:, supported:)
time_frame = metrics.map { |metric| metric.time_frame.description }.join('/')
unique_by = "unique #{identifier}s " if identifier
event_phrase = EVENT_PHRASES[identifier] % event_name
filter_phrase = " where filtered" if filter_name
# @option defined [Boolean] whether this metric already exists
# @option supported [Boolean] whether unique metrics are supported for this identifier
Option = Struct.new(:identifier, :events_name, :filter_name, :metrics, :defined, :supported,
keyword_init: true) do
include InternalEventsCli::Helpers::Formatting
if supported && !defined
filter_phrase = " #{format_info('where')} #{filter_name} is..." if filter_name
time_frame = format_info(time_frame)
unique_by = format_info(unique_by)
# @return [Hash] see #get_metric_options for format
# ex) Monthly/Weekly count of unique users who triggered cli_template_included where label/property is...
# ex) Monthly/Weekly count of unique users who triggered cli_template_included (user unavailable)
def formatted
name = [time_frame_phrase, identifier_phrase, filter_phrase].compact.join(' ')
name = format_help(name) if disabled
{ name: name, disabled: disabled, value: metrics }.compact
end
name = "#{time_frame} count of #{unique_by}[#{event_phrase}]#{filter_phrase}"
if supported && defined
disabled = format_warning("(already defined)")
name = format_help(name)
elsif !supported
disabled = format_warning("(#{identifier} unavailable)")
name = format_help(name)
def identifier
Metric::Identifier.new(self[:identifier])
end
{ name: name, value: metrics, disabled: disabled }.compact
# ex) "Monthly/Weekly"
def time_frame_phrase
phrase = metrics.map { |metric| metric.time_frame.description }.join('/')
disabled ? phrase : format_info(phrase)
end
# ex) "count of unique users who triggered cli_template_included"
def identifier_phrase
phrase = identifier.description % events_name
phrase.gsub!(unique_phrase, format_info(unique_phrase)) unless disabled
phrase
end
# ex) "unique users"
def unique_phrase
"unique #{identifier.plural}"
end
# ex) "where label/property is..."
def filter_phrase
return unless filter_name
return "where filtered" if disabled
"#{format_info("where #{filter_name}")} is..."
end
# Returns the string to include at the end of disabled
# menu items. Nil if menu item shouldn't be disabled
def disabled
if defined
pastel.bold(format_help("(already defined)"))
elsif !supported
pastel.bold(format_help("(#{identifier.value} unavailable)"))
end
end
end
end
end

View File

@ -129,7 +129,7 @@ module InternalEventsCli
def event_params(action, filter = nil)
params = { 'name' => action }
params['unique'] = "#{identifier.value}.id" if identifier.value
params['unique'] = identifier.reference if identifier.value
params['filter'] = filter if filter&.any?
params
@ -156,23 +156,24 @@ module InternalEventsCli
filters.expected?
end
# Automatically prepended to all new descriptions
# ex) Total count of
# ex) Weekly/Monthly count of unique
def description_prefix
[time_frame.description, identifier.description].join(' ')
[
time_frame.description,
identifier.prefix,
*(identifier.plural if identifier.default?)
].join(' ')
end
# Provides simplified but technically accurate description
# to be used before the user has provided a description
def technical_description
event_name = actions.first if events.length == 1 && !filtered?
event_name ||= 'the selected events'
case identifier.value
when 'user'
"#{description_prefix} who triggered #{event_name}"
when 'project', 'namespace'
"#{description_prefix} where #{event_name} occurred"
else
"#{description_prefix} #{event_name} occurrences"
end
"#{time_frame.description} #{identifier.description % event_name}"
end
def bulk_assign(key_value_pairs)
@ -203,16 +204,48 @@ module InternalEventsCli
end
Identifier = Struct.new(:value) do
# returns a description of the identifier with appropriate
# grammer to interpolate a description of events
def description
if value.nil?
"#{prefix} %s occurrences"
elsif value == 'user'
"#{prefix} users who triggered %s"
elsif %w[project namespace].include?(value)
"#{prefix} #{plural} where %s occurred"
else
"#{prefix} #{plural} from %s occurrences"
end
end
# handles generic pluralization for unknown indentifers
def plural
default? ? "#{value}s" : "values for '#{value}'"
end
def prefix
if value
"count of unique #{value}s"
"count of unique"
else
"count of"
end
end
# returns a slug which can be used in the
# metric's key_path and filepath
def key_path
value ? "distinct_#{value}_id_from" : 'total'
value ? "distinct_#{reference.tr('.', '_')}_from" : 'total'
end
# Returns the identifier string that will be included in the yml
def reference
default? ? "#{value}.id" : value
end
# Refers to the top-level identifiers not included in
# additional_properties
def default?
%w[user project namespace].include?(value)
end
end

View File

@ -142,7 +142,10 @@ module InternalEventsCli
'Which metrics do you want to add?',
eligible_metrics,
**select_opts,
per_page: 20)
**filter_opts,
per_page: 20,
&disabled_format_callback
)
assign_shared_attrs(:actions, :milestone) do
{
@ -155,6 +158,7 @@ module InternalEventsCli
def prompt_for_event_filters
return if @metrics.none?(&:filters_expected?)
selected_unique_identifier = @metrics.first.identifier.value
event_count = selected_events.length
previous_inputs = {
'label' => nil,
@ -168,6 +172,8 @@ module InternalEventsCli
next if deselect_nonfilterable_event?(event) # prompts user
filter_values = event.additional_properties&.filter_map do |property, _|
next if selected_unique_identifier == property
prompt_for_property_filter(
event.action,
property,
@ -459,6 +465,7 @@ module InternalEventsCli
prompt_for_text(" Finish the description: #{description_start}", default, multiline: true) do |q|
q.required true
q.modify :trim
q.messages[:required?] = Text::METRIC_DESCRIPTION_HELP
end
end

View File

@ -60,12 +60,8 @@ RSpec.describe 'Group empty states', feature_category: :groups_and_projects do
wait_for_all_requests
within_testid('issuable-empty-state') do
expect(page).to have_content(/Sorry, your filter produced no results/)
new_issuable_path = issuable == :issue ? 'new_project_issue_path' : 'project_new_merge_request_path'
path = public_send(new_issuable_path, project)
expect(page.find('a')['href']).to have_content(path)
empty_state_title = issuable == :issue ? "Sorry, your filter produced no results" : "No results found"
expect(page).to have_content(empty_state_title)
end
end

View File

@ -67,7 +67,7 @@ RSpec.describe 'Group merge requests page', feature_category: :code_review_workf
filtered_search.set(search_term)
filtered_search.send_keys(:enter)
expect(page).to have_content('filter produced no results')
expect(page).to have_content('No results found')
expect(page).to have_link('Open', href: "/groups/#{group.name}/-/merge_requests?scope=all&search=#{search_term}&state=opened")
end
end

View File

@ -35,8 +35,8 @@ RSpec.describe 'Merge request > User sees empty state', feature_category: :code_
visit project_merge_requests_path(project, milestone_title: "1.0")
expect(page).to have_selector('.gl-empty-state')
expect(page).to have_content('Sorry, your filter produced no results')
expect(page).to have_content('To widen your search, change or remove filters above')
expect(page).to have_content('No results found')
expect(page).to have_content('To widen your search, change or remove filters above.')
end
end
@ -49,13 +49,10 @@ RSpec.describe 'Merge request > User sees empty state', feature_category: :code_
sign_in(fork_user)
end
it 'shows an empty state and a "New merge request" button' do
it 'shows empty state when filter results empty' do
visit project_merge_requests_path(project, search: 'foo')
expect(page).to have_selector('.gl-empty-state')
within('.gl-empty-state') do
expect(page).to have_link 'New merge request', href: project_new_merge_request_path(forked_project)
end
end
end
end

View File

@ -94,8 +94,8 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
wait_for_requests
end
it 'renders "Run pipeline" link' do
expect(page).to have_link('Run pipeline')
it 'renders "New pipeline" link' do
expect(page).to have_link('New pipeline')
end
end

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used_monthly
description: Monthly count of unique values provided for label
product_group: analytics_instrumentation
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: label

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_label_from_internal_events_cli_used_weekly
description: Weekly count of unique values provided for label
product_group: analytics_instrumentation
performance_indicator_type: []
value_type: number
status: active
milestone: '16.6'
introduced_by_url: TODO
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: internal_events_cli_used
unique: label

View File

@ -315,7 +315,7 @@
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
- "internal_events_cli_used" # Filters to this event
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\e[B" # Arrow down to: Weekly count of unique users where label/value is...
- "\e[B\e[B\e[B" # Arrow down to: Weekly count of unique users where label/value is...
- "\n" # Select: Weekly count of unique users where label/value is...
- "failure, incomplete\n" # Input multiple values for "label" filter
- "\n" # Skip "value" filter
@ -344,7 +344,7 @@
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
- "internal_events_cli_used" # Filters to this event
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\e[B" # Arrow down to: Weekly count of unique users where label/value is...
- "\e[B\e[B\e[B" # Arrow down to: Weekly count of unique users where label/value is...
- "\n" # Select: Weekly count of unique users where label/value is...
- "failure, incomplete\n" # Input multiple values for "label" filter
- "metrics, events\n" # Input multiple values for "property" filter
@ -429,3 +429,28 @@
files:
- path: config/metrics/counts_all/count_total_cli_interactions.yml
content: spec/fixtures/scripts/internal_events/metrics/total_multiple_events_with_rename.yml
- description: Create weekly/monthly metrics for a single event unique by 'label'
inputs:
files:
- path: config/events/internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/events/event_with_additional_properties.yml
keystrokes:
- "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
- "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
- "internal_events_cli_used" # Filters to this event
- "\n" # Select: config/events/internal_events_cli_used.yml
- "\e[A\e[A\e[A\e[A" # Arrow up to: Weekly count of unique values for label
- "\n" # Select: Weekly count of unique values for label
- "values provided for label\n" # Input description
- "\n" # Submit monthly description for weekly
- "1\n" # Enum-select: Copy & continue
- "y\n" # Create file
- "y\n" # Create file
- "5\n" # Exit
outputs:
files:
- path: config/metrics/counts_28d/count_distinct_label_from_internal_events_cli_used_monthly.yml
content: spec/fixtures/scripts/internal_events/metrics/label_28d_single_event_additional_props.yml
- path: config/metrics/counts_7d/count_distinct_label_from_internal_events_cli_used_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/label_7d_single_event_additional_props.yml

View File

@ -24,7 +24,7 @@ describe('Pipelines Nav Controls', () => {
createComponent(mockData);
const runPipelineButton = findRunPipelineButton();
expect(runPipelineButton.text()).toContain('Run pipeline');
expect(runPipelineButton.text()).toContain('New pipeline');
expect(runPipelineButton.attributes('href')).toBe(mockData.newPipelinePath);
});

View File

@ -29,7 +29,7 @@ RSpec.describe StatAnchorsHelper, feature_category: :groups_and_projects do
let(:anchor) { anchor_klass.new(false) }
it 'returns the proper attributes' do
expect(subject[:class]).to include('stat-link !gl-px-0 !gl-pb-2 btn-link gl-button !gl-text-blue-500')
expect(subject[:class]).to include('stat-link !gl-px-0 !gl-pb-2 btn-link gl-button !gl-text-link')
end
end
end

View File

@ -495,6 +495,24 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
end
end
context 'when unique key is an additional property' do
let(:event_selection_rules) do
[
Gitlab::Usage::EventSelectionRule.new(name: event_name, time_framed: false),
Gitlab::Usage::EventSelectionRule.new(name: event_name, time_framed: true),
Gitlab::Usage::EventSelectionRule.new(name: event_name, time_framed: true, unique_identifier_name: :label)
]
end
it 'is used when logging to RedisHLL', :aggregate_failures do
described_class.track_event(event_name, user: user, project: project, label: 'label')
expect_redis_tracking
expect_redis_hll_tracking('label'.hash, :label)
expect_snowplow_tracking
end
end
context 'when send_snowplow_event is false' do
it 'logs to Redis and RedisHLL but not Snowplow' do
described_class.track_event(event_name, send_snowplow_event: false, user: user, project: project)

View File

@ -35,6 +35,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 2, time: 2.weeks.ago, property_name: 'user')
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 1, time: 2.weeks.ago, property_name: 'user')
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 3, time: 2.weeks.ago, property_name: 'project')
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:g_project_management_issue_iteration_changed, values: 3, time: 2.weeks.ago, property_name: 'label')
end
it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', events: [name: 'g_project_management_issue_iteration_changed', unique: 'user.id'] }
@ -59,6 +60,13 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_
context "with options attributes also defined" do
it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', options: { events: ['i_quickactions_approve'] }, events: [name: 'g_project_management_issue_iteration_changed', unique: 'user.id'] }
end
context 'with property_name excluding ".id"' do
let(:expected_value) { 1 }
it_behaves_like 'a correct instrumented metric value',
{ time_frame: '28d', events: [name: 'g_project_management_issue_iteration_changed', unique: 'label'] }
end
end
describe 'children classes' do

View File

@ -0,0 +1,279 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../../../scripts/internal_events/cli'
RSpec.describe InternalEventsCli::Helpers::MetricOptions::Option, feature_category: :service_ping do
let(:identifier) { 'user' }
let(:events_name) { 'a list of events' }
let(:filter_name) { nil }
let(:defined) { false }
let(:supported) { true }
let(:styling_stub) { Pastel.new }
let(:metrics) do
[instance_double(InternalEventsCli::NewMetric,
time_frame: instance_double(InternalEventsCli::Metric::TimeFrame, description: "time frame"))]
end
subject(:option) do
described_class.new(
identifier: identifier,
events_name: events_name,
filter_name: filter_name,
metrics: metrics,
defined: defined,
supported: supported
)
end
before do
[:cyan, :yellow, :green, :bright_black, :magenta, :red, :bold].each do |color|
allow(styling_stub).to receive(color).and_wrap_original do |method, arg|
"<#{method.name}>#{arg}</#{method.name}>"
end
end
allow(Pastel).to receive(:new).and_return(styling_stub)
end
context 'when option is for a supported and not yet defined metric' do
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events",
value: metrics
})
end
context 'with a filter' do
let(:filter_name) { 'label/prop/anything' }
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of <cyan>unique users</cyan> who triggered a list of events " \
"<cyan>where label/prop/anything</cyan> is...",
value: metrics
})
end
end
end
context 'when option is already defined' do
let(:defined) { true }
it 'formats the option as disabled' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered a list of events</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(already defined)</bright_black></bold>"
})
end
context 'with a filter' do
let(:filter_name) { 'label/prop/anything' }
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered " \
"a list of events where filtered</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(already defined)</bright_black></bold>"
})
end
end
end
context 'when option is not supported' do
let(:supported) { false }
it 'formats the option as disabled' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered a list of events</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(user unavailable)</bright_black></bold>"
})
end
context 'with a filter' do
let(:filter_name) { 'label/prop/anything' }
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<bright_black>time frame count of unique users who triggered " \
"a list of events where filtered</bright_black>",
value: metrics,
disabled: "<bold><bright_black>(user unavailable)</bright_black></bold>"
})
end
end
end
context 'when identifier is an additional_property' do
let(:identifier) { 'label' }
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of <cyan>unique values for 'label'</cyan> " \
"from a list of events occurrences",
value: metrics
})
end
end
context 'with no identifier' do
let(:identifier) { nil }
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame</cyan> count of a list of events occurrences",
value: metrics
})
end
end
context 'with multiple metrics' do
let(:metrics) do
[
instance_double(InternalEventsCli::NewMetric,
time_frame: instance_double(InternalEventsCli::Metric::TimeFrame, description: "time frame 1")),
instance_double(InternalEventsCli::NewMetric,
time_frame: instance_double(InternalEventsCli::Metric::TimeFrame, description: "time frame 2"))
]
end
it 'highlights key words in the name' do
expect(option.formatted).to eq({
name: "<cyan>time frame 1/time frame 2</cyan> count of <cyan>unique users</cyan> " \
"who triggered a list of events",
value: metrics
})
end
end
end
RSpec.describe InternalEventsCli::Helpers::MetricOptions::EventSelection, feature_category: :service_ping do
let(:event_1) do
instance_double(
InternalEventsCli::ExistingEvent,
action: 'action_1',
available_filters: ['property'],
identifiers: %w[user namespace]
)
end
let(:event_2) do
instance_double(
InternalEventsCli::ExistingEvent,
action: 'action_2',
available_filters: %w[property value],
identifiers: %w[user project namespace]
)
end
let(:event_3) do
instance_double(
InternalEventsCli::ExistingEvent,
action: 'action_3',
available_filters: [],
identifiers: ['user']
)
end
let(:events) { [event_1, event_2, event_3] }
subject(:selection) { described_class.new(events) }
context 'with one event selected' do
let(:events) { [event_2] }
it 'reflects the full capabilities of a metric', :aggregate_failures do
expect(selection.actions).to contain_exactly('action_2')
expect(selection.events_name).to eq('action_2')
expect(selection.filter_name('user')).to eq('property/value')
expect(selection.filter_name('value')).to eq('property')
expect(selection.shared_filters).to contain_exactly('property', 'value')
expect(selection.filter_options).to contain_exactly('property', 'value')
expect(selection.shared_identifiers).to contain_exactly('user', 'project', 'namespace')
expect(selection.uniqueness_options).to contain_exactly('user', 'project', 'namespace', 'property', 'value', nil)
expect(selection.can_be_unique?('user')).to be(true)
expect(selection.can_be_unique?('label')).to be(false)
expect(selection.can_be_unique?(nil)).to be(true)
expect(selection.can_filter_when_unique?('value')).to be(true)
expect(selection.can_filter_when_unique?('label')).to be(false)
expect(selection.can_filter_when_unique?('user')).to be(true)
expect(selection.exclude_filter_identifier?('property')).to be(false)
expect(selection.exclude_filter_identifier?('value')).to be(false)
expect(selection.exclude_filter_identifier?('label')).to be(false)
expect(selection.exclude_filter_identifier?('user')).to be(false)
end
end
context 'with multiple events selected' do
let(:events) { [event_1, event_2] }
it 'restricts based on common attributes between the metrics', :aggregate_failures do
expect(selection.actions).to contain_exactly('action_1', 'action_2')
expect(selection.events_name).to eq('any of 2 events')
expect(selection.filter_name('user')).to eq('property/value')
expect(selection.filter_name('value')).to eq('property')
expect(selection.shared_filters).to contain_exactly('property')
expect(selection.filter_options).to contain_exactly('property', 'value')
expect(selection.shared_identifiers).to contain_exactly('user', 'namespace')
expect(selection.uniqueness_options).to contain_exactly('user', 'namespace', 'property', nil)
expect(selection.can_be_unique?('user')).to be(true)
expect(selection.can_be_unique?('property')).to be(true)
expect(selection.can_be_unique?('project')).to be(false)
expect(selection.can_filter_when_unique?('value')).to be(false)
expect(selection.can_filter_when_unique?('label')).to be(false)
expect(selection.can_filter_when_unique?('property')).to be(true)
expect(selection.can_filter_when_unique?('user')).to be(true)
expect(selection.exclude_filter_identifier?('property')).to be(false)
expect(selection.exclude_filter_identifier?('value')).to be(false)
expect(selection.exclude_filter_identifier?('label')).to be(false)
expect(selection.exclude_filter_identifier?('user')).to be(false)
expect(selection.exclude_filter_identifier?(nil)).to be(false)
end
end
context 'with even more events selected' do
let(:events) { [event_1, event_2, event_3] }
it 'restricts based on common attributes between the metrics', :aggregate_failures do
expect(selection.actions).to contain_exactly('action_1', 'action_2', 'action_3')
expect(selection.events_name).to eq('any of 3 events')
expect(selection.filter_name('user')).to eq('property/value')
expect(selection.filter_name('value')).to eq('property')
expect(selection.shared_filters).to be_empty
expect(selection.filter_options).to contain_exactly('property', 'value')
expect(selection.shared_identifiers).to contain_exactly('user')
expect(selection.uniqueness_options).to contain_exactly('user', nil)
expect(selection.can_be_unique?('user')).to be(true)
expect(selection.can_be_unique?('property')).to be(false)
expect(selection.can_be_unique?('project')).to be(false)
expect(selection.can_filter_when_unique?('value')).to be(false)
expect(selection.can_filter_when_unique?('label')).to be(false)
expect(selection.can_filter_when_unique?('property')).to be(false)
expect(selection.can_filter_when_unique?('user')).to be(true)
expect(selection.exclude_filter_identifier?('property')).to be(false)
expect(selection.exclude_filter_identifier?('value')).to be(false)
expect(selection.exclude_filter_identifier?('label')).to be(false)
expect(selection.exclude_filter_identifier?('user')).to be(false)
end
end
end

View File

@ -0,0 +1,135 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../../scripts/internal_events/cli'
RSpec.describe InternalEventsCli::NewMetric, :aggregate_failures, feature_category: :service_ping do
let(:time_frame) { '7d' }
let(:identifier) { 'user' }
let(:actions) { ['action_1'] }
let(:filters) { nil }
subject(:metric) do
described_class.new(
time_frame: time_frame,
identifier: identifier,
actions: actions,
filters: filters
)
end
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique users')
expect(metric.technical_description).to eq('Weekly count of unique users who triggered action_1')
end
context 'when filtered' do
let(:filters) { [] }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique users')
expect(metric.technical_description).to eq('Weekly count of unique users who triggered the selected events')
end
end
context 'when all time' do
let(:time_frame) { 'all' }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Total count of unique users')
expect(metric.technical_description).to eq('Total count of unique users who triggered action_1')
end
end
context 'with multiple events' do
let(:actions) { %w[action_1 action_2] }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique users')
expect(metric.technical_description).to eq('Weekly count of unique users who triggered the selected events')
end
end
context 'when unique by default identifier' do
let(:identifier) { 'project' }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique projects')
expect(metric.technical_description).to eq('Weekly count of unique projects where action_1 occurred')
end
context 'when filtered' do
let(:filters) { [] }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique projects')
expect(metric.technical_description).to eq('Weekly count of unique projects where the selected events occurred')
end
end
end
context 'when unique by additional property' do
let(:identifier) { 'label' }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique')
expect(metric.technical_description).to eq("Weekly count of unique values for 'label' from action_1 occurrences")
end
context 'when filtered' do
let(:filters) { [] }
it 'has expected description content' do
expect(metric.description_prefix).to eq('Weekly count of unique')
expect(metric.technical_description).to eq(
"Weekly count of unique values for 'label' from the selected events occurrences"
)
end
end
end
end
RSpec.describe InternalEventsCli::Metric::Identifier, :aggregate_failures, feature_category: :service_ping do
subject(:identifier) { described_class.new(value) }
context 'with no value' do
let(:value) { nil }
it 'has expected components' do
expect(identifier.value).to eq(nil)
expect(identifier.description).to eq('count of %s occurrences')
expect(identifier.key_path).to eq('total')
end
end
context 'when value is user' do
let(:value) { 'user' }
it 'has expected components' do
expect(identifier.value).to eq('user')
expect(identifier.description).to eq('count of unique users who triggered %s')
expect(identifier.key_path).to eq('distinct_user_id_from')
end
end
context 'when value is an identifier' do
let(:value) { 'namespace' }
it 'has expected components' do
expect(identifier.value).to eq('namespace')
expect(identifier.description).to eq('count of unique namespaces where %s occurred')
expect(identifier.key_path).to eq('distinct_namespace_id_from')
end
end
context 'when value is an additional property' do
let(:value) { 'label' }
it 'has expected components' do
expect(identifier.value).to eq('label')
expect(identifier.description).to eq("count of unique values for 'label' from %s occurrences")
expect(identifier.key_path).to eq('distinct_label_from')
end
end
end

View File

@ -275,11 +275,11 @@ RSpec.describe Cli, feature_category: :service_ping do
select_event_from_list
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
Monthly/Weekly count of [internal_events_cli_used occurrences]
Total count of [internal_events_cli_used occurrences]
Monthly/Weekly count of unique users who triggered internal_events_cli_used
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
Monthly/Weekly count of internal_events_cli_used occurrences
Total count of internal_events_cli_used occurrences
TEXT
with_cli_thread do
@ -299,12 +299,12 @@ RSpec.describe Cli, feature_category: :service_ping do
select_event_from_list
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
Monthly count of [internal_events_cli_used occurrences]
Weekly count of [internal_events_cli_used occurrences] (already defined)
Total count of [internal_events_cli_used occurrences]
Monthly/Weekly count of unique users who triggered internal_events_cli_used
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
Monthly count of internal_events_cli_used occurrences
Total count of internal_events_cli_used occurrences
Weekly count of internal_events_cli_used occurrences (already defined)
TEXT
with_cli_thread do
@ -325,11 +325,11 @@ RSpec.describe Cli, feature_category: :service_ping do
select_event_from_list
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users [who triggered internal_events_cli_used]
Monthly/Weekly count of unique projects [where internal_events_cli_used occurred]
Monthly/Weekly count of unique namespaces [where internal_events_cli_used occurred]
Monthly/Weekly count of [internal_events_cli_used occurrences]
Total count of [internal_events_cli_used occurrences] (already defined)
Monthly/Weekly count of unique users who triggered internal_events_cli_used
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
Monthly/Weekly count of internal_events_cli_used occurrences
Total count of internal_events_cli_used occurrences (already defined)
TEXT
with_cli_thread do
@ -386,11 +386,11 @@ RSpec.describe Cli, feature_category: :service_ping do
])
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users [who triggered any of 2 events]
Monthly/Weekly count of unique projects [where any of 2 events occurred] (already defined)
Monthly/Weekly count of unique namespaces [where any of 2 events occurred]
Monthly/Weekly count of [any of 2 events occurrences]
Total count of [any of 2 events occurrences]
Monthly/Weekly count of unique users who triggered any of 2 events
Monthly/Weekly count of unique namespaces where any of 2 events occurred
Monthly/Weekly count of any of 2 events occurrences
Total count of any of 2 events occurrences
Monthly/Weekly count of unique projects where any of 2 events occurred (already defined)
TEXT
with_cli_thread do
@ -413,11 +413,11 @@ RSpec.describe Cli, feature_category: :service_ping do
])
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users [who triggered internal_events_cli_opened] (user unavailable)
Monthly/Weekly count of unique projects [where internal_events_cli_opened occurred] (project unavailable)
Monthly/Weekly count of unique namespaces [where internal_events_cli_opened occurred] (namespace unavailable)
Monthly/Weekly count of [internal_events_cli_opened occurrences]
Total count of [internal_events_cli_opened occurrences]
Monthly/Weekly count of internal_events_cli_opened occurrences
Total count of internal_events_cli_opened occurrences
Monthly/Weekly count of unique users who triggered internal_events_cli_opened (user unavailable)
Monthly/Weekly count of unique projects where internal_events_cli_opened occurred (project unavailable)
Monthly/Weekly count of unique namespaces where internal_events_cli_opened occurred (namespace unavailable)
TEXT
with_cli_thread do
@ -460,6 +460,117 @@ RSpec.describe Cli, feature_category: :service_ping do
end
end
end
context 'when additional properties are present' do
let(:event_path_with_add_props) { 'config/events/internal_events_cli_used.yml' }
let(:event_content_with_add_props) { internal_event_fixture('events/event_with_all_additional_properties.yml') }
before do
File.write(event_path_with_add_props, File.read(event_content_with_add_props))
end
it 'offers metrics to filter by or count unique additional props' do
queue_cli_inputs([
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
"1\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
'internal_events_cli_used', # Filters to this event
"\n" # Select: config/events/internal_events_cli_used.yml
])
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users who triggered internal_events_cli_used
Monthly/Weekly count of unique projects where internal_events_cli_used occurred
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred
Monthly/Weekly count of unique users who triggered internal_events_cli_used where label/property/value is...
Monthly/Weekly count of unique projects where internal_events_cli_used occurred where label/property/value is...
Monthly/Weekly count of unique namespaces where internal_events_cli_used occurred where label/property/value is...
Monthly/Weekly count of internal_events_cli_used occurrences
Monthly/Weekly count of internal_events_cli_used occurrences where label/property/value is...
Total count of internal_events_cli_used occurrences
Total count of internal_events_cli_used occurrences where label/property/value is...
Monthly/Weekly count of unique values for 'label' from internal_events_cli_used occurrences
Monthly/Weekly count of unique values for 'property' from internal_events_cli_used occurrences
Monthly/Weekly count of unique values for 'value' from internal_events_cli_used occurrences
Monthly/Weekly count of unique values for 'label' from internal_events_cli_used occurrences where property/value is...
Monthly/Weekly count of unique values for 'property' from internal_events_cli_used occurrences where label/value is...
Monthly/Weekly count of unique values for 'value' from internal_events_cli_used occurrences where label/property is...
TEXT
with_cli_thread do
expect { plain_last_lines(16) }.to eventually_equal_cli_text(expected_output)
end
end
context 'with multiple events' do
let(:another_event_path) { 'config/events/internal_events_cli_opened.yml' }
let(:another_event_content) { internal_event_fixture('events/secondary_event_with_additional_properties.yml') }
before do
File.write(another_event_path, File.read(another_event_content))
end
it 'disables unique metrics without shared additional props, but allows filtered metrics' do
queue_cli_inputs([
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
"2\n", # Enum-select: Single event -- count occurrences of a specific event or user interaction
"internal_events_cli_", # Filters to this event
" ", # Select: config/events/internal_events_cli_used.yml
"\e[B ", # Arrow down & Select: config/events/internal_events_cli_opened.yml
"\n" # Submit Multi-select
])
# Note the disabled "property" field is deduplicated with the filtered option
# The event only has label/value defined, so we'll include those
expected_output = <<~TEXT.chomp
Monthly/Weekly count of unique users who triggered any of 2 events
Monthly/Weekly count of unique projects where any of 2 events occurred
Monthly/Weekly count of unique namespaces where any of 2 events occurred
Monthly/Weekly count of unique users who triggered any of 2 events where label/value/property is...
Monthly/Weekly count of unique projects where any of 2 events occurred where label/value/property is...
Monthly/Weekly count of unique namespaces where any of 2 events occurred where label/value/property is...
Monthly/Weekly count of any of 2 events occurrences
Monthly/Weekly count of any of 2 events occurrences where label/value/property is...
Total count of any of 2 events occurrences
Total count of any of 2 events occurrences where label/value/property is...
Monthly/Weekly count of unique values for 'label' from any of 2 events occurrences
Monthly/Weekly count of unique values for 'value' from any of 2 events occurrences
Monthly/Weekly count of unique values for 'label' from any of 2 events occurrences where value/property is...
Monthly/Weekly count of unique values for 'value' from any of 2 events occurrences where label/property is...
Monthly/Weekly count of unique values for 'property' from any of 2 events occurrences (property unavailable)
TEXT
with_cli_thread do
expect { plain_last_lines(15) }.to eventually_equal_cli_text(expected_output)
end
end
it 'skips filter inputs for an unavailable property' do
queue_cli_inputs([
"2\n", # Enum-select: New Metric -- calculate how often one or more existing events occur over time
"2\n", # Enum-select: Multiple events -- count occurrences of a specific event or user interaction
"internal_events_cli_", # Filters to this event
" ", # Select: config/events/internal_events_cli_used.yml
"\e[B ", # Arrow down & Select: config/events/internal_events_cli_opened.yml
"\n", # Submit Multi-select
"\e[A\n", # Arrow up & select Monthly/Weekly unique 'value' from any of 2 events where label/property is...
"a label value\n", # Enter a value for 'label' for internal_events_cli_opened
"\n", # Accept the same 'label' value for internal_events_cli_used
"a property value\n", # Enter a value for 'property' for internal_events_cli_used
"here's a description\n", # Submit a description
"heres_a_key\n" # Submit a replacement key path for filtered metric
])
# 'value' is an additional property for the metric here,
# so proceeding to the next step without that extra input means we filtered
with_cli_thread do
expect { plain_last_lines }.to eventually_include_cli_text(
'internal_events_cli_opened(label=a label value)',
'internal_events_cli_used(label=a label value property=a property value)'
)
end
end
end
end
end
context 'when showing usage examples' do

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :runner do
subject(:execute) { described_class.new(runner).execute(params) }
let(:runner) { create(:ci_runner) }
let(:runner) { create(:ci_runner, tag_list: %w[macos shared]) }
before do
allow(runner).to receive(:tick_runner_queue)
@ -24,6 +24,29 @@ RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :
end
end
context 'with tag_list param' do
using RSpec::Parameterized::TableSyntax
where(:tag_list, :expected_tag_list) do
[] | []
['macos'] | ['macos']
['linux'] | ['linux']
end
with_them do
let(:params) { { tag_list: tag_list } }
it 'updates the runner and ticking the queue' do
expect(execute).to be_success
runner.reload
expect(runner).to have_received(:tick_runner_queue)
expect(runner.tag_list).to eq(expected_tag_list)
end
end
end
context 'with paused param' do
let(:params) { { paused: true } }
@ -52,6 +75,7 @@ RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :
end
context 'when params are not valid' do
let(:runner) { create(:ci_runner) }
let(:params) { { run_untagged: false } }
it 'does not update and returns error because it is not valid' do

View File

@ -1,48 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Onboarding::ProgressService, feature_category: :onboarding do
describe '#execute' do
let(:namespace) { create(:namespace) }
let(:action) { :namespace_action }
subject(:execute_service) { described_class.new(namespace).execute(action: :merge_request_created) }
context 'when the namespace is a root' do
before do
Onboarding::Progress.onboard(namespace)
end
it 'registers a namespace onboarding progress action for the given namespace' do
execute_service
expect(Onboarding::Progress.completed?(namespace, :merge_request_created)).to eq(true)
end
end
context 'when the namespace is not the root' do
let(:group) { create(:group, :nested) }
before do
Onboarding::Progress.onboard(group)
end
it 'does not register a namespace onboarding progress action' do
execute_service
expect(Onboarding::Progress.completed?(group, :merge_request_created)).to be(false)
end
end
context 'when no namespace is passed' do
let(:namespace) { nil }
it 'does not register a namespace onboarding progress action' do
execute_service
expect(Onboarding::Progress.completed?(namespace, :merge_request_created)).to be(false)
end
end
end
end

View File

@ -17,12 +17,6 @@ RSpec.describe Projects::UpdateStatisticsService, feature_category: :groups_and_
service.execute
end
it_behaves_like 'does not record an onboarding progress action' do
subject do
service.execute
end
end
end
context 'with an existing project' do

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'records an onboarding progress action' do |action|
include AfterNextHelpers
it do
expect_next(Onboarding::ProgressService, namespace)
.to receive(:execute).with(action: action).and_call_original
subject
end
end
RSpec.shared_examples 'does not record an onboarding progress action' do
it do
expect(Onboarding::ProgressService).not_to receive(:new)
subject
end
end

View File

@ -138,16 +138,6 @@ RSpec.describe 'keep-around tasks', :silence_stdout, feature_category: :source_c
orphan_count: 2
end
context "for sent notification keep-arounds" do
let_it_be(:sent_notification) do
create(:sent_notification, project: project, commit_id: ::TestEnv::BRANCH_SHA['master'])
end
it_behaves_like 'orphans found',
keep_around_count: 3,
orphan_count: 2
end
context "for todo keep-arounds" do
let_it_be(:todo) { create(:todo, project: project, commit_id: ::TestEnv::BRANCH_SHA['master']) }

View File

@ -1,59 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe MergeRequests::EnsurePreparedWorker, :sidekiq_inline, feature_category: :code_review_workflow do
subject(:worker) { described_class.new }
let_it_be(:merge_request_1, reload: true) { create(:merge_request, prepared_at: :nil) }
let_it_be(:merge_request_2, reload: true) { create(:merge_request, prepared_at: Time.current) }
let_it_be(:merge_request_3, reload: true) { create(:merge_request, prepared_at: :nil) }
describe '#perform' do
context 'when ensure_merge_requests_prepared is enabled' do
it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
worker.perform
expect(merge_request_1.reload.prepared_at).not_to eq(nil)
expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.reload.prepared_at).not_to eq(nil)
end
end
context 'when ensure_merge_requests_prepared is disabled' do
before do
stub_feature_flags(ensure_merge_requests_prepared: false)
end
it 'does not prepare any merge requests' do
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
worker.perform
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
end
end
end
it_behaves_like 'an idempotent worker' do
it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do
expect(merge_request_1.prepared_at).to eq(nil)
expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.prepared_at).to eq(nil)
subject
expect(merge_request_1.reload.prepared_at).not_to eq(nil)
expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at)
expect(merge_request_3.reload.prepared_at).not_to eq(nil)
end
end
end

View File

@ -3,11 +3,7 @@
require 'spec_helper'
RSpec.describe Onboarding::IssueCreatedWorker, '#perform', feature_category: :onboarding do
let_it_be(:issue) { create(:issue) }
let(:namespace) { issue.project.namespace }
it_behaves_like 'does not record an onboarding progress action' do
subject { described_class.new.perform(namespace.id) }
specify do
expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error
end
end

View File

@ -3,11 +3,7 @@
require 'spec_helper'
RSpec.describe Onboarding::PipelineCreatedWorker, '#perform', feature_category: :onboarding do
let_it_be(:ci_pipeline) { create(:ci_pipeline) }
it_behaves_like 'does not record an onboarding progress action' do
let(:namespace) { ci_pipeline.project.namespace }
subject { described_class.new.perform(ci_pipeline.project.namespace_id) }
specify do
expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error
end
end

View File

@ -3,10 +3,7 @@
require 'spec_helper'
RSpec.describe Onboarding::ProgressWorker, '#perform', feature_category: :onboarding do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:action) { 'git_write' }
it_behaves_like 'does not record an onboarding progress action' do
subject { described_class.new.perform(namespace.id, action) }
specify do
expect { described_class.new.perform(non_existing_record_id, '_action_') }.not_to raise_error
end
end

View File

@ -3,9 +3,7 @@
require 'spec_helper'
RSpec.describe Onboarding::UserAddedWorker, '#perform', feature_category: :onboarding do
let_it_be(:namespace) { create(:group) }
subject { described_class.new.perform(namespace.id) }
it_behaves_like 'does not record an onboarding progress action'
specify do
expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error
end
end