Commit c87b87ff authored by Luis Mejia's avatar Luis Mejia

Merge branch 'master' of gitlab.com:gitlab-org/gitlab into...

Merge branch 'master' of gitlab.com:gitlab-org/gitlab into 333117-migrate-quickactions-events-redishll
parents 2cfc880d 2ad17865
...@@ -49,6 +49,7 @@ ...@@ -49,6 +49,7 @@
"Geo", "Geo",
"Git LFS", "Git LFS",
"git-annex", "git-annex",
"git-sizer",
"Git", "Git",
"Gitaly", "Gitaly",
"GitHub", "GitHub",
......
0a4e1c785063f4ad2cef80303fa10276e2e4e2a6 b3d56404cc25983d1bffd015fe0d29c1d50eab58
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/url_utility'; import { getParameterByName } from '~/lib/utils/url_utility';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { PipelineKeyOptions } from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub'; import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin'; import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service'; import PipelinesService from '~/pipelines/services/pipelines_service';
...@@ -10,6 +11,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination ...@@ -10,6 +11,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
PipelineKeyOptions,
components: { components: {
GlButton, GlButton,
GlEmptyState, GlEmptyState,
...@@ -205,6 +207,7 @@ export default { ...@@ -205,6 +207,7 @@ export default {
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:view-type="viewType" :view-type="viewType"
:pipeline-key-option="$options.PipelineKeyOptions[0]"
> >
<template #table-header-actions> <template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right"> <div v-if="canRenderPipelineButton" class="gl-text-right">
......
...@@ -29,6 +29,10 @@ export default { ...@@ -29,6 +29,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
pipelineKey: {
type: String,
required: true,
},
}, },
computed: { computed: {
user() { user() {
...@@ -60,7 +64,7 @@ export default { ...@@ -60,7 +64,7 @@ export default {
data-testid="pipeline-url-link" data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link" data-qa-selector="pipeline_url_link"
> >
#{{ pipeline.id }} #{{ pipeline[pipelineKey] }}
</gl-link> </gl-link>
<div class="label-container"> <div class="label-container">
<gl-badge <gl-badge
......
<script> <script>
import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getParameterByName } from '~/lib/utils/url_utility'; import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; import {
ANY_TRIGGER_AUTHOR,
RAW_TEXT_WARNING,
FILTER_TAG_IDENTIFIER,
PipelineKeyOptions,
} from '../../constants';
import PipelinesMixin from '../../mixins/pipelines_mixin'; import PipelinesMixin from '../../mixins/pipelines_mixin';
import PipelinesService from '../../services/pipelines_service'; import PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils'; import { validateParams } from '../../utils';
...@@ -16,8 +21,11 @@ import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; ...@@ -16,8 +21,11 @@ import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import PipelinesTableComponent from './pipelines_table.vue'; import PipelinesTableComponent from './pipelines_table.vue';
export default { export default {
PipelineKeyOptions,
components: { components: {
EmptyState, EmptyState,
GlDropdown,
GlDropdownItem,
GlEmptyState, GlEmptyState,
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
...@@ -114,6 +122,7 @@ export default { ...@@ -114,6 +122,7 @@ export default {
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
requestData: {}, requestData: {},
isResetCacheButtonLoading: false, isResetCacheButtonLoading: false,
selectedPipelineKeyOption: this.$options.PipelineKeyOptions[0],
}; };
}, },
stateMap: { stateMap: {
...@@ -301,6 +310,9 @@ export default { ...@@ -301,6 +310,9 @@ export default {
this.updateContent(this.requestData); this.updateContent(this.requestData);
}, },
changeVisibilityPipelineID(val) {
this.selectedPipelineKeyOption = val;
},
}, },
}; };
</script> </script>
...@@ -330,12 +342,31 @@ export default { ...@@ -330,12 +342,31 @@ export default {
/> />
</div> </div>
<pipelines-filtered-search <div v-if="stateToRender !== $options.stateMap.emptyState" class="gl-display-flex">
v-if="stateToRender !== $options.stateMap.emptyState" <div class="row-content-block gl-display-flex gl-flex-grow-1">
:project-id="projectId" <pipelines-filtered-search
:params="validatedParams" class="gl-display-flex gl-flex-grow-1 gl-mr-4"
@filterPipelines="filterPipelines" :project-id="projectId"
/> :params="validatedParams"
@filterPipelines="filterPipelines"
/>
<gl-dropdown
class="gl-display-flex"
:text="selectedPipelineKeyOption.text"
data-testid="pipeline-key-dropdown"
>
<gl-dropdown-item
v-for="(val, index) in $options.PipelineKeyOptions"
:key="index"
:is-checked="selectedPipelineKeyOption.key === val.key"
is-check-item
@click="changeVisibilityPipelineID(val)"
>
{{ val.text }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
<div class="content-list pipelines"> <div class="content-list pipelines">
<gl-loading-icon <gl-loading-icon
...@@ -374,6 +405,7 @@ export default { ...@@ -374,6 +405,7 @@ export default {
:pipeline-schedule-url="pipelineScheduleUrl" :pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:view-type="viewType" :view-type="viewType"
:pipeline-key-option="selectedPipelineKeyOption"
/> />
</div> </div>
......
...@@ -101,12 +101,10 @@ export default { ...@@ -101,12 +101,10 @@ export default {
</script> </script>
<template> <template>
<div class="row-content-block"> <gl-filtered-search
<gl-filtered-search v-model="value"
v-model="value" :placeholder="__('Filter pipelines')"
:placeholder="__('Filter pipelines')" :available-tokens="tokens"
:available-tokens="tokens" @submit="onSubmit"
@submit="onSubmit" />
/>
</div>
</template> </template>
...@@ -17,63 +17,6 @@ const DEFAULT_TH_CLASSES = ...@@ -17,63 +17,6 @@ const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!'; 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!';
export default { export default {
fields: [
{
key: 'status',
label: s__('Pipeline|Status'),
thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-10p',
tdClass: DEFAULT_TD_CLASS,
thAttr: { 'data-testid': 'status-th' },
},
{
key: 'pipeline',
label: s__('Pipeline|Pipeline'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'pipeline-th' },
},
{
key: 'triggerer',
label: s__('Pipeline|Triggerer'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
key: 'commit',
label: s__('Pipeline|Commit'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'commit-th' },
},
{
key: 'stages',
label: s__('Pipeline|Stages'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'timeago',
label: s__('Pipeline|Duration'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'timeago-th' },
},
{
key: 'actions',
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'actions-th' },
},
],
components: { components: {
GlTable, GlTable,
LinkedPipelinesMiniList: () => LinkedPipelinesMiniList: () =>
...@@ -109,6 +52,10 @@ export default { ...@@ -109,6 +52,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
pipelineKeyOption: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -118,6 +65,68 @@ export default { ...@@ -118,6 +65,68 @@ export default {
cancelingPipeline: null, cancelingPipeline: null,
}; };
}, },
computed: {
tableFields() {
const fields = [
{
key: 'status',
label: s__('Pipeline|Status'),
thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-10p',
tdClass: DEFAULT_TD_CLASS,
thAttr: { 'data-testid': 'status-th' },
},
{
key: 'pipeline',
label: this.pipelineKeyOption.label,
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'pipeline-th' },
},
{
key: 'triggerer',
label: s__('Pipeline|Triggerer'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
key: 'commit',
label: s__('Pipeline|Commit'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'commit-th' },
},
{
key: 'stages',
label: s__('Pipeline|Stages'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'timeago',
label: s__('Pipeline|Duration'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'timeago-th' },
},
{
key: 'actions',
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'actions-th' },
},
];
return fields;
},
},
watch: { watch: {
pipelines() { pipelines() {
this.cancelingPipeline = null; this.cancelingPipeline = null;
...@@ -148,7 +157,7 @@ export default { ...@@ -148,7 +157,7 @@ export default {
<template> <template>
<div class="ci-table"> <div class="ci-table">
<gl-table <gl-table
:fields="$options.fields" :fields="tableFields"
:items="pipelines" :items="pipelines"
tbody-tr-class="commit" tbody-tr-class="commit"
:tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }" :tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }"
...@@ -169,7 +178,11 @@ export default { ...@@ -169,7 +178,11 @@ export default {
</template> </template>
<template #cell(pipeline)="{ item }"> <template #cell(pipeline)="{ item }">
<pipeline-url :pipeline="item" :pipeline-schedule-url="pipelineScheduleUrl" /> <pipeline-url
:pipeline="item"
:pipeline-schedule-url="pipelineScheduleUrl"
:pipeline-key="pipelineKeyOption.key"
/>
</template> </template>
<template #cell(triggerer)="{ item }"> <template #cell(triggerer)="{ item }">
......
...@@ -35,3 +35,17 @@ export const POST_FAILURE = 'post_failure'; ...@@ -35,3 +35,17 @@ export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data'; export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child'; export const CHILD_VIEW = 'child';
// Constants for the ID and IID selection dropdown
export const PipelineKeyOptions = [
{
text: __('Show Pipeline ID'),
label: __('Pipeline ID'),
key: 'id',
},
{
text: __('Show Pipeline IID'),
label: __('Pipeline IID'),
key: 'iid',
},
];
...@@ -54,8 +54,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -54,8 +54,6 @@ class ProjectsController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def new def new
return access_denied! unless current_user.can_create_project?
@namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace) return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace)
......
...@@ -267,11 +267,7 @@ module Nav ...@@ -267,11 +267,7 @@ module Nav
builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path) builder.add_primary_menu_item(id: 'your', title: _('Your projects'), href: dashboard_projects_path)
builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path) builder.add_primary_menu_item(id: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path) builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_path)
builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
if current_user.can_create_project?
builder.add_secondary_menu_item(id: 'create', title: _('Create new project'), href: new_project_path)
end
builder.build builder.build
end end
......
# frozen_string_literal: true
module Analytics
module CycleAnalytics
class StageEventHash < ApplicationRecord
has_many :cycle_analytics_project_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :stage_event_hash
validates :hash_sha256, presence: true
# Creates or queries the id of the corresponding stage event hash code
def self.record_id_by_hash_sha256(hash)
casted_hash_code = Arel::Nodes.build_quoted(hash, Analytics::CycleAnalytics::StageEventHash.arel_table[:hash_sha256]).to_sql
# Atomic, safe insert without retrying
query = <<~SQL
WITH insert_cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
INSERT INTO #{quoted_table_name} (hash_sha256) VALUES (#{casted_hash_code}) ON CONFLICT DO NOTHING RETURNING ID
)
SELECT ids.id FROM (
(SELECT id FROM #{quoted_table_name} WHERE hash_sha256=#{casted_hash_code} LIMIT 1)
UNION ALL
(SELECT id FROM insert_cte LIMIT 1)
) AS ids LIMIT 1
SQL
connection.execute(query).first['id']
end
def self.cleanup_if_unused(id)
unused_hashes_for(id)
.where(id: id)
.delete_all
end
def self.unused_hashes_for(id)
exists_query = Analytics::CycleAnalytics::ProjectStage.where(stage_event_hash_id: id).select('1').limit(1)
where.not('EXISTS (?)', exists_query)
end
end
end
end
Analytics::CycleAnalytics::StageEventHash.prepend_mod_with('Analytics::CycleAnalytics::StageEventHash')
...@@ -151,7 +151,7 @@ module Ci ...@@ -151,7 +151,7 @@ module Ci
scope :with_project_and_metadata, -> do scope :with_project_and_metadata, -> do
if Feature.enabled?(:non_public_artifacts, type: :development) if Feature.enabled?(:non_public_artifacts, type: :development)
joins(:metadata).includes(:project, :metadata) joins(:metadata).includes(:metadata).preload(:project)
end end
end end
......
...@@ -10,6 +10,7 @@ module Analytics ...@@ -10,6 +10,7 @@ module Analytics
included do included do
belongs_to :start_event_label, class_name: 'GroupLabel', optional: true belongs_to :start_event_label, class_name: 'GroupLabel', optional: true
belongs_to :end_event_label, class_name: 'GroupLabel', optional: true belongs_to :end_event_label, class_name: 'GroupLabel', optional: true
belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', foreign_key: :stage_event_hash_id, optional: true
validates :name, presence: true validates :name, presence: true
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
...@@ -28,6 +29,9 @@ module Analytics ...@@ -28,6 +29,9 @@ module Analytics
scope :ordered, -> { order(:relative_position, :id) } scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered } scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) } scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
before_save :ensure_stage_event_hash_id
after_commit :cleanup_old_stage_event_hash
end end
def parent=(_) def parent=(_)
...@@ -133,6 +137,20 @@ module Analytics ...@@ -133,6 +137,20 @@ module Analytics
.id_in(label_id) .id_in(label_id)
.exists? .exists?
end end
def ensure_stage_event_hash_id
previous_stage_event_hash = stage_event_hash&.hash_sha256
if previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash
self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code)
end
end
def cleanup_old_stage_event_hash
if stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was
Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was)
end
end
end end
end end
end end
...@@ -14,9 +14,10 @@ class WebHookWorker ...@@ -14,9 +14,10 @@ class WebHookWorker
worker_has_external_dependencies! worker_has_external_dependencies!
def perform(hook_id, data, hook_name) def perform(hook_id, data, hook_name)
hook = WebHook.find(hook_id) hook = WebHook.find_by_id(hook_id)
data = data.with_indifferent_access return unless hook
data = data.with_indifferent_access
WebHookService.new(hook, data, hook_name, jid).execute WebHookService.new(hook, data, hook_name, jid).execute
end end
end end
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- p_terraform_state_api_unique_users
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_committing_ciconfigfile
distribution: distribution:
- ee - ee
- ce - ce
......
...@@ -12,6 +12,10 @@ milestone: "13.10" ...@@ -12,6 +12,10 @@ milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54707 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54707
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -12,6 +12,10 @@ milestone: '13.11' ...@@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_secure_security_report
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -12,7 +12,12 @@ status: data_available ...@@ -12,7 +12,12 @@ status: data_available
milestone: "13.12" milestone: "13.12"
introduced_by_url: introduced_by_url:
time_frame: 28d time_frame: 28d
data_source: data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_committing_ciconfigfile
- o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_committing_ciconfigfile
distribution: distribution:
- ee - ee
- ce - ce
......
...@@ -12,6 +12,10 @@ milestone: "13.10" ...@@ -12,6 +12,10 @@ milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54707 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54707
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -12,6 +12,10 @@ milestone: '13.11' ...@@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_secure_security_report
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -12,7 +12,12 @@ status: data_available ...@@ -12,7 +12,12 @@ status: data_available
milestone: "13.12" milestone: "13.12"
introduced_by_url: introduced_by_url:
time_frame: 7d time_frame: 7d
data_source: data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_committing_ciconfigfile
- o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile
distribution: distribution:
- ce - ce
- ee - ee
......
# frozen_string_literal: true
class CreateAnalyticsCycleAnalyticsStageEventHashes < ActiveRecord::Migration[6.1]
def change
create_table :analytics_cycle_analytics_stage_event_hashes do |t|
t.binary :hash_sha256
t.index :hash_sha256, unique: true, name: 'index_cycle_analytics_stage_event_hashes_on_hash_sha_256'
end
end
end
# frozen_string_literal: true
class AddStageHashFkToProjectStages < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
unless column_exists?(:analytics_cycle_analytics_project_stages, :stage_event_hash_id)
add_column :analytics_cycle_analytics_project_stages, :stage_event_hash_id, :bigint
end
add_concurrent_index :analytics_cycle_analytics_project_stages, :stage_event_hash_id, name: 'index_project_stages_on_stage_event_hash_id'
add_concurrent_foreign_key :analytics_cycle_analytics_project_stages, :analytics_cycle_analytics_stage_event_hashes, column: :stage_event_hash_id, on_delete: :cascade
end
def down
remove_column :analytics_cycle_analytics_project_stages, :stage_event_hash_id
end
end
# frozen_string_literal: true
class AddStageHashFkToGroupStages < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
unless column_exists?(:analytics_cycle_analytics_group_stages, :stage_event_hash_id)
add_column :analytics_cycle_analytics_group_stages, :stage_event_hash_id, :bigint
end
add_concurrent_index :analytics_cycle_analytics_group_stages, :stage_event_hash_id, name: 'index_group_stages_on_stage_event_hash_id'
add_concurrent_foreign_key :analytics_cycle_analytics_group_stages, :analytics_cycle_analytics_stage_event_hashes, column: :stage_event_hash_id, on_delete: :cascade
end
def down
remove_column :analytics_cycle_analytics_group_stages, :stage_event_hash_id
end
end
f819eaed7e387f18f066180cbf9d0849b3e38db95bbf3e8487d3bc58d9b489ae
\ No newline at end of file
cb97b869bfb0b76dd0684aca1f40c86e7c1c9c9a0d52684830115288088e8066
\ No newline at end of file
5c104ffdb64943aa4828a9b961c8f9141dfd2ae861cea7116722d2b0d4598957
\ No newline at end of file
...@@ -9085,7 +9085,8 @@ CREATE TABLE analytics_cycle_analytics_group_stages ( ...@@ -9085,7 +9085,8 @@ CREATE TABLE analytics_cycle_analytics_group_stages (
hidden boolean DEFAULT false NOT NULL, hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true NOT NULL, custom boolean DEFAULT true NOT NULL,
name character varying(255) NOT NULL, name character varying(255) NOT NULL,
group_value_stream_id bigint NOT NULL group_value_stream_id bigint NOT NULL,
stage_event_hash_id bigint
); );
CREATE SEQUENCE analytics_cycle_analytics_group_stages_id_seq CREATE SEQUENCE analytics_cycle_analytics_group_stages_id_seq
...@@ -9128,7 +9129,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages ( ...@@ -9128,7 +9129,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages (
hidden boolean DEFAULT false NOT NULL, hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true NOT NULL, custom boolean DEFAULT true NOT NULL,
name character varying(255) NOT NULL, name character varying(255) NOT NULL,
project_value_stream_id bigint NOT NULL project_value_stream_id bigint NOT NULL,
stage_event_hash_id bigint
); );
CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
...@@ -9158,6 +9160,20 @@ CREATE SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq ...@@ -9158,6 +9160,20 @@ CREATE SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq
ALTER SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq OWNED BY analytics_cycle_analytics_project_value_streams.id; ALTER SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq OWNED BY analytics_cycle_analytics_project_value_streams.id;
CREATE TABLE analytics_cycle_analytics_stage_event_hashes (
id bigint NOT NULL,
hash_sha256 bytea
);
CREATE SEQUENCE analytics_cycle_analytics_stage_event_hashes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE analytics_cycle_analytics_stage_event_hashes_id_seq OWNED BY analytics_cycle_analytics_stage_event_hashes.id;
CREATE TABLE analytics_devops_adoption_segments ( CREATE TABLE analytics_devops_adoption_segments (
id bigint NOT NULL, id bigint NOT NULL,
last_recorded_at timestamp with time zone, last_recorded_at timestamp with time zone,
...@@ -19925,6 +19941,8 @@ ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DE ...@@ -19925,6 +19941,8 @@ ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DE
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_value_streams_id_seq'::regclass); ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_value_streams_id_seq'::regclass);
ALTER TABLE ONLY analytics_cycle_analytics_stage_event_hashes ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_stage_event_hashes_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass); ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_snapshots ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_snapshots_id_seq'::regclass); ALTER TABLE ONLY analytics_devops_adoption_snapshots ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_snapshots_id_seq'::regclass);
...@@ -21044,6 +21062,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_project_stages ...@@ -21044,6 +21062,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
ADD CONSTRAINT analytics_cycle_analytics_project_value_streams_pkey PRIMARY KEY (id); ADD CONSTRAINT analytics_cycle_analytics_project_value_streams_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_cycle_analytics_stage_event_hashes
ADD CONSTRAINT analytics_cycle_analytics_stage_event_hashes_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER TABLE ONLY analytics_devops_adoption_segments
ADD CONSTRAINT analytics_devops_adoption_segments_pkey PRIMARY KEY (id); ADD CONSTRAINT analytics_devops_adoption_segments_pkey PRIMARY KEY (id);
...@@ -23536,6 +23557,8 @@ CREATE INDEX index_custom_emoji_on_creator_id ON custom_emoji USING btree (creat ...@@ -23536,6 +23557,8 @@ CREATE INDEX index_custom_emoji_on_creator_id ON custom_emoji USING btree (creat
CREATE UNIQUE INDEX index_custom_emoji_on_namespace_id_and_name ON custom_emoji USING btree (namespace_id, name); CREATE UNIQUE INDEX index_custom_emoji_on_namespace_id_and_name ON custom_emoji USING btree (namespace_id, name);
CREATE UNIQUE INDEX index_cycle_analytics_stage_event_hashes_on_hash_sha_256 ON analytics_cycle_analytics_stage_event_hashes USING btree (hash_sha256);
CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_daily_build_group_report_results USING btree (project_id, ref_path, date, group_name); CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON ci_daily_build_group_report_results USING btree (project_id, ref_path, date, group_name);
CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at); CREATE INDEX index_dast_profile_schedules_active_next_run_at ON dast_profile_schedules USING btree (active, next_run_at);
...@@ -23960,6 +23983,8 @@ CREATE INDEX index_group_repository_storage_moves_on_group_id ON group_repositor ...@@ -23960,6 +23983,8 @@ CREATE INDEX index_group_repository_storage_moves_on_group_id ON group_repositor
CREATE UNIQUE INDEX index_group_stages_on_group_id_group_value_stream_id_and_name ON analytics_cycle_analytics_group_stages USING btree (group_id, group_value_stream_id, name); CREATE UNIQUE INDEX index_group_stages_on_group_id_group_value_stream_id_and_name ON analytics_cycle_analytics_group_stages USING btree (group_id, group_value_stream_id, name);
CREATE INDEX index_group_stages_on_stage_event_hash_id ON analytics_cycle_analytics_group_stages USING btree (stage_event_hash_id);
CREATE UNIQUE INDEX index_group_wiki_repositories_on_disk_path ON group_wiki_repositories USING btree (disk_path); CREATE UNIQUE INDEX index_group_wiki_repositories_on_disk_path ON group_wiki_repositories USING btree (disk_path);
CREATE INDEX index_group_wiki_repositories_on_shard_id ON group_wiki_repositories USING btree (shard_id); CREATE INDEX index_group_wiki_repositories_on_shard_id ON group_wiki_repositories USING btree (shard_id);
...@@ -24762,6 +24787,8 @@ CREATE INDEX index_project_settings_on_project_id_partially ON project_settings ...@@ -24762,6 +24787,8 @@ CREATE INDEX index_project_settings_on_project_id_partially ON project_settings
CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings USING btree (push_rule_id); CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON project_settings USING btree (push_rule_id);
CREATE INDEX index_project_stages_on_stage_event_hash_id ON analytics_cycle_analytics_project_stages USING btree (stage_event_hash_id);
CREATE INDEX index_project_statistics_on_namespace_id ON project_statistics USING btree (namespace_id); CREATE INDEX index_project_statistics_on_namespace_id ON project_statistics USING btree (namespace_id);
CREATE INDEX index_project_statistics_on_packages_size_and_project_id ON project_statistics USING btree (packages_size, project_id); CREATE INDEX index_project_statistics_on_packages_size_and_project_id ON project_statistics USING btree (packages_size, project_id);
...@@ -26073,6 +26100,9 @@ ALTER TABLE ONLY members ...@@ -26073,6 +26100,9 @@ ALTER TABLE ONLY members
ALTER TABLE ONLY lfs_objects_projects ALTER TABLE ONLY lfs_objects_projects
ADD CONSTRAINT fk_2eb33f7a78 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE NOT VALID; ADD CONSTRAINT fk_2eb33f7a78 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY analytics_cycle_analytics_group_stages
ADD CONSTRAINT fk_3078345d6d FOREIGN KEY (stage_event_hash_id) REFERENCES analytics_cycle_analytics_stage_event_hashes(id) ON DELETE CASCADE;
ALTER TABLE ONLY lists ALTER TABLE ONLY lists
ADD CONSTRAINT fk_30f2a831f4 FOREIGN KEY (iteration_id) REFERENCES sprints(id) ON DELETE CASCADE; ADD CONSTRAINT fk_30f2a831f4 FOREIGN KEY (iteration_id) REFERENCES sprints(id) ON DELETE CASCADE;
...@@ -26514,6 +26544,9 @@ ALTER TABLE ONLY packages_packages ...@@ -26514,6 +26544,9 @@ ALTER TABLE ONLY packages_packages
ALTER TABLE ONLY geo_event_log ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_c1f241c70d FOREIGN KEY (upload_deleted_event_id) REFERENCES geo_upload_deleted_events(id) ON DELETE CASCADE; ADD CONSTRAINT fk_c1f241c70d FOREIGN KEY (upload_deleted_event_id) REFERENCES geo_upload_deleted_events(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT fk_c3339bdfc9 FOREIGN KEY (stage_event_hash_id) REFERENCES analytics_cycle_analytics_stage_event_hashes(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_exports ALTER TABLE ONLY vulnerability_exports
ADD CONSTRAINT fk_c3d3cb5d0f FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_c3d3cb5d0f FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
...@@ -123,6 +123,9 @@ From there, you can see the following actions: ...@@ -123,6 +123,9 @@ From there, you can see the following actions:
- Created, updated, or deleted DAST profiles, DAST scanner profiles, and DAST site profiles - Created, updated, or deleted DAST profiles, DAST scanner profiles, and DAST site profiles
([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217872) in GitLab 14.1) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217872) in GitLab 14.1)
- Changed a project's compliance framework ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329362) in GitLab 14.1) - Changed a project's compliance framework ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329362) in GitLab 14.1)
- User password required for approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2)
- Permission to modify merge requests approval rules in merge requests was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2)
- New approvals requirement when new commits are added to an MR was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2)
Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events). Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events).
......
...@@ -49,7 +49,7 @@ If a job shouldn't be able to be triggered from chat, you can set the job to `ex ...@@ -49,7 +49,7 @@ If a job shouldn't be able to be triggered from chat, you can set the job to `ex
Since ChatOps is built upon GitLab CI/CD, the job has all the same features and Since ChatOps is built upon GitLab CI/CD, the job has all the same features and
functions available. Consider these best practices when creating ChatOps jobs: functions available. Consider these best practices when creating ChatOps jobs:
- GitLab strongly recommends you set `only: [chat]` so the job does not run as part - GitLab strongly recommends you set [`rules`](../yaml/index.md#rules) so the job does not run as part
of the standard CI pipeline. of the standard CI pipeline.
- If the job is set to `when: manual`, ChatOps creates the pipeline, but the job waits to be started. - If the job is set to `when: manual`, ChatOps creates the pipeline, but the job waits to be started.
- ChatOps provides limited support for access control. If the user triggering the - ChatOps provides limited support for access control. If the user triggering the
...@@ -65,9 +65,13 @@ The output for jobs with a single command is sent to the channel as a reply. For ...@@ -65,9 +65,13 @@ The output for jobs with a single command is sent to the channel as a reply. For
example, the chat reply of the following job is `Hello World` in the channel: example, the chat reply of the following job is `Hello World` in the channel:
```yaml ```yaml
stages:
- chatops
hello-world: hello-world:
stage: chatops stage: chatops
only: [chat] rules:
- if: '$CI_PIPELINE_SOURCE == "chat"'
script: script:
- echo "Hello World" - echo "Hello World"
``` ```
...@@ -81,9 +85,13 @@ the `chat_reply` section. For example, the following job lists the files in the ...@@ -81,9 +85,13 @@ the `chat_reply` section. For example, the following job lists the files in the
current directory: current directory:
```yaml ```yaml
stages:
- chatops
ls: ls:
stage: chatops stage: chatops
only: [chat] rules:
- if: '$CI_PIPELINE_SOURCE == "chat"'
script: script:
- echo "This command will not be shown." - echo "This command will not be shown."
- echo -e "section_start:$( date +%s ):chat_reply\r\033[0K\n$( ls -la )\nsection_end:$( date +%s ):chat_reply\r\033[0K" - echo -e "section_start:$( date +%s ):chat_reply\r\033[0K\n$( ls -la )\nsection_end:$( date +%s ):chat_reply\r\033[0K"
......
...@@ -14,15 +14,16 @@ GitLab CI/CD can be used with Bitbucket Cloud by: ...@@ -14,15 +14,16 @@ GitLab CI/CD can be used with Bitbucket Cloud by:
To use GitLab CI/CD with a Bitbucket Cloud repository: To use GitLab CI/CD with a Bitbucket Cloud repository:
1. <!-- vale gitlab.Spelling = NO --> In GitLab create a **CI/CD for external repository**, select 1. In GitLab, create a project:
**Repo by URL** and create the project. 1. On the top menu, select **Projects > Create new project**.
<!-- vale gitlab.Spelling = YES --> 1. Select **Run CI/CD for external repository**.
1. Select **Repo by URL**.
![Create project](img/external_repository.png) ![Create project](img/external_repository.png)
GitLab imports the repository and enables [Pull Mirroring](../../user/project/repository/repository_mirroring.md#pull-from-a-remote-repository). GitLab imports the repository and enables [Pull Mirroring](../../user/project/repository/repository_mirroring.md#pull-from-a-remote-repository).
1. In GitLab create a 1. In GitLab, create a
[Personal Access Token](../../user/profile/personal_access_tokens.md) [Personal Access Token](../../user/profile/personal_access_tokens.md)
with `api` scope. This is used to authenticate requests from the web with `api` scope. This is used to authenticate requests from the web
hook that is created in Bitbucket to notify GitLab of new commits. hook that is created in Bitbucket to notify GitLab of new commits.
...@@ -120,7 +121,7 @@ To use GitLab CI/CD with a Bitbucket Cloud repository: ...@@ -120,7 +121,7 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
\"$BITBUCKET_DESCRIPTION\",\"url\": \"$CI_PROJECT_URL/-/jobs/$CI_JOB_ID\" }" \"$BITBUCKET_DESCRIPTION\",\"url\": \"$CI_PROJECT_URL/-/jobs/$CI_JOB_ID\" }"
``` ```
1. Still in Bitbucket, create a `.gitlab-ci.yml` file to use the script to push 1. In Bitbucket, create a `.gitlab-ci.yml` file to use the script to push
pipeline success and failures to Bitbucket. pipeline success and failures to Bitbucket.
```yaml ```yaml
......
...@@ -27,27 +27,26 @@ repositories to GitLab, and the GitHub user must have the [owner role](https://d ...@@ -27,27 +27,26 @@ repositories to GitLab, and the GitHub user must have the [owner role](https://d
To perform a one-off authorization with GitHub to grant GitLab access your To perform a one-off authorization with GitHub to grant GitLab access your
repositories: repositories:
1. Open <https://github.com/settings/tokens/new> to create a **Personal Access 1. In GitHub, create a token:
Token**. This token is used to access your repository and push commit 1. Open <https://github.com/settings/tokens/new>.
statuses to GitHub. 1. Create a **Personal Access Token**.
1. Enter a **Token description** and update the scope to allow
The `repo` and `admin:repo_hook` should be enable to allow GitLab access to `repo` and `admin:repo_hook` so that GitLab can access your project,
your project, update commit statuses, and create a web hook to notify update commit statuses, and create a web hook to notify GitLab of new commits.
GitLab of new commits. 1. In GitLab, create a project:
1. On the top menu, select **Projects > Create new project**.
1. In GitLab, go to the [new project page](../../user/project/working_with_projects.md#create-a-project), select the **CI/CD for external repository** tab, and then click 1. Select **Run CI/CD for external repository**.
**GitHub**. 1. Select **GitHub**.
1. For **Personal access token**, paste the token.
1. Paste the token into the **Personal access token** field and click **List 1. Select **List Repositories**.
Repositories**. Click **Connect** to select the repository. 1. Select **Connect** to select the repository.
1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/index.md). 1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/index.md).
GitLab: GitLab:
1. Imports the project. 1. Imports the project.
1. Enables [Pull Mirroring](../../user/project/repository/repository_mirroring.md#pull-from-a-remote-repository) 1. Enables [Pull Mirroring](../../user/project/repository/repository_mirroring.md#pull-from-a-remote-repository).
1. Enables [GitHub project integration](../../user/project/integrations/github.md) 1. Enables [GitHub project integration](../../user/project/integrations/github.md).
1. Creates a web hook on GitHub to notify GitLab of new commits. 1. Creates a web hook on GitHub to notify GitLab of new commits.
## Connect manually ## Connect manually
...@@ -56,30 +55,25 @@ To use **GitHub Enterprise** with **GitLab.com**, use this method. ...@@ -56,30 +55,25 @@ To use **GitHub Enterprise** with **GitLab.com**, use this method.
To manually enable GitLab CI/CD for your repository: To manually enable GitLab CI/CD for your repository:
1. In GitHub open <https://github.com/settings/tokens/new> create a **Personal 1. In GitHub, create a token:
Access Token.** GitLab uses this token to access your repository and 1. Open <https://github.com/settings/tokens/new>.
push commit statuses. 1. Create a **Personal Access Token**.
1. Enter a **Token description** and update the scope to allow
Enter a **Token description** and update the scope to allow: `repo` so that GitLab can access your project and update commit statuses.
1. In GitLab, create a project:
`repo` so that GitLab can access your project and update commit statuses 1. On the top menu, select **Projects > Create new project**.
1. Select **Run CI/CD for external repository** and **Repo by URL**.
1. In GitLab create a **CI/CD project** using the Git URL option and the HTTPS 1. In the **Git repository URL** field, enter the HTTPS URL for your GitHub repository.
URL for your GitHub repository. If your project is private, use the personal If your project is private, use the personal access token you just created for authentication.
access token you just created for authentication. 1. Fill in all the other fields and select **Create project**.
GitLab automatically configures polling-based pull mirroring.
GitLab automatically configures polling-based pull mirroring. 1. In GitLab, enable [GitHub project integration](../../user/project/integrations/github.md):
1. On the left sidebar, select **Settings > Integrations**.
1. Still in GitLab, enable the [GitHub project integration](../../user/project/integrations/github.md) 1. Select the **Active** checkbox.
from **Settings > Integrations.** 1. Paste your personal access token and HTTPS repository URL into the form and select **Save**.
1. In GitLab, create a **Personal Access Token** with `API` scope to
Check the **Active** checkbox to enable the integration, paste your
personal access token and HTTPS repository URL into the form, and **Save.**
1. Still in GitLab create a **Personal Access Token** with `API` scope to
authenticate the GitHub web hook notifying GitLab of new commits. authenticate the GitHub web hook notifying GitLab of new commits.
1. In GitHub, from **Settings > Webhooks**, create a web hook to notify GitLab of
1. In GitHub from **Settings > Webhooks** create a web hook to notify GitLab of
new commits. new commits.
The web hook URL should be set to the GitLab API to The web hook URL should be set to the GitLab API to
...@@ -92,7 +86,7 @@ To manually enable GitLab CI/CD for your repository: ...@@ -92,7 +86,7 @@ To manually enable GitLab CI/CD for your repository:
Select the **Let me select individual events** option, then check the **Pull requests** and **Pushes** checkboxes. These settings are needed for [pipelines for external pull requests](index.md#pipelines-for-external-pull-requests). Select the **Let me select individual events** option, then check the **Pull requests** and **Pushes** checkboxes. These settings are needed for [pipelines for external pull requests](index.md#pipelines-for-external-pull-requests).
1. In GitHub add a `.gitlab-ci.yml` to configure GitLab CI/CD. 1. In GitHub, add a `.gitlab-ci.yml` to configure GitLab CI/CD.
<!-- ## Troubleshooting <!-- ## Troubleshooting
......
...@@ -27,10 +27,10 @@ To connect to an external repository: ...@@ -27,10 +27,10 @@ To connect to an external repository:
<!-- vale gitlab.Spelling = NO --> <!-- vale gitlab.Spelling = NO -->
1. From your GitLab dashboard, click **New project**. 1. On the top menu, select **Projects > Create new project**.
1. Switch to the **CI/CD for external repository** tab. 1. Select **Run CI/CD for external repository**.
1. Choose **GitHub** or **Repo by URL**. 1. Select **GitHub** or **Repo by URL**.
1. The next steps are similar to the [import flow](../../user/project/import/index.md). 1. Complete the fields.
<!-- vale gitlab.Spelling = YES --> <!-- vale gitlab.Spelling = YES -->
......
...@@ -123,6 +123,9 @@ you can filter the pipeline list by: ...@@ -123,6 +123,9 @@ you can filter the pipeline list by:
- Status ([GitLab 13.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/217617)) - Status ([GitLab 13.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/217617))
- Tag ([GitLab 13.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/217617)) - Tag ([GitLab 13.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/217617))
[Starting in GitLab 14.2](https://gitlab.com/gitlab-org/gitlab/-/issues/26621), you can change the
pipeline column to display the pipeline ID or the pipeline IID.
### Run a pipeline manually ### Run a pipeline manually
Pipelines can be manually executed, with predefined or manually-specified [variables](../variables/index.md). Pipelines can be manually executed, with predefined or manually-specified [variables](../variables/index.md).
...@@ -167,6 +170,9 @@ variables: ...@@ -167,6 +170,9 @@ variables:
You cannot set job-level variables to be pre-filled when you run a pipeline manually. You cannot set job-level variables to be pre-filled when you run a pipeline manually.
Pre-filled variables do not show up when the CI/CD configuration is [external to the project](settings.md#specify-a-custom-cicd-configuration-file).
See the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336184) for more details.
### Run a pipeline by using a URL query string ### Run a pipeline by using a URL query string
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24146) in GitLab 12.5. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24146) in GitLab 12.5.
......
...@@ -122,8 +122,13 @@ If the CI/CD configuration file is on an external site, the URL must end with `. ...@@ -122,8 +122,13 @@ If the CI/CD configuration file is on an external site, the URL must end with `.
- `http://example.com/generate/ci/config.yml` - `http://example.com/generate/ci/config.yml`
If the CI/CD configuration file is in a different project, the path must be relative If the CI/CD configuration file is in a different project:
to the root directory in the other project. Include the group and project name at the end:
- The file must exist on its default branch.
- The path must be relative to the root directory in the other project.
- The path must include the group and project name at the end.
For example:
- `.gitlab-ci.yml@mygroup/another-project` - `.gitlab-ci.yml@mygroup/another-project`
- `my/path/.my-custom-file.yml@mygroup/another-project` - `my/path/.my-custom-file.yml@mygroup/another-project`
......
...@@ -1530,13 +1530,16 @@ production: ...@@ -1530,13 +1530,16 @@ production:
#### Requirements and limitations #### Requirements and limitations
- In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632) - In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632) you
you can refer to jobs in the same stage as the job you are configuring. This feature can refer to jobs in the same stage as the job you are configuring. This feature is:
is [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
- Disabled on GitLab.com. - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
- Not recommended for production use. - Disabled on GitLab.com.
- For GitLab self-managed instances, GitLab adminsitrators - Not recommended for production use.
can choose to [disable it](#enable-or-disable-needs-for-jobs-in-the-same-stage)
For GitLab self-managed instances, GitLab administrators can choose to
[enable it](#enable-or-disable-needs-for-jobs-in-the-same-stage).
- In GitLab 14.0 and older, you can only refer to jobs in earlier stages. - In GitLab 14.0 and older, you can only refer to jobs in earlier stages.
- In GitLab 13.9 and older, if `needs:` refers to a job that might not be added to - In GitLab 13.9 and older, if `needs:` refers to a job that might not be added to
a pipeline because of `only`, `except`, or `rules`, the pipeline might fail to create. a pipeline because of `only`, `except`, or `rules`, the pipeline might fail to create.
......
...@@ -72,12 +72,17 @@ issue should have one and only one. ...@@ -72,12 +72,17 @@ issue should have one and only one.
The current type labels are: The current type labels are:
- ~feature - `~feature`
- ~bug - `~"feature::addition"`
- ~tooling - `~"feature::enhancement"`
- ~"support request" - `~"feature::maintenance"`
- ~meta - `~bug`
- ~documentation - `~tooling`
- `~"tooling::pipelines"`
- `~"tooling::workflow"`
- `~"support request"`
- `~meta`
- `~documentation`
A number of type labels have a priority assigned to them, which automatically A number of type labels have a priority assigned to them, which automatically
makes them float to the top, depending on their importance. makes them float to the top, depending on their importance.
......
...@@ -113,7 +113,7 @@ patterns may apply to future cases. ...@@ -113,7 +113,7 @@ patterns may apply to future cases.
The simplest solution we've seen several times now has been an existing scope The simplest solution we've seen several times now has been an existing scope
that is unused. This is the easiest example to fix. So the first step is to that is unused. This is the easiest example to fix. So the first step is to
investigate if the code is unused and then simply remove it. These are some investigate if the code is unused and then remove it. These are some
real examples: real examples:
- <https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67162> - <https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67162>
...@@ -131,6 +131,20 @@ to evaluate, because `UsageData` is not critical to users and it may be possible ...@@ -131,6 +131,20 @@ to evaluate, because `UsageData` is not critical to users and it may be possible
to get a similarly useful metric with a simpler approach. Alternatively we may to get a similarly useful metric with a simpler approach. Alternatively we may
find that nobody is using these metrics, so we can remove them. find that nobody is using these metrics, so we can remove them.
#### Use `preload` instead of `includes`
The `includes` and `preload` methods in Rails are both ways to avoid an N+1
query. The `includes` method in Rails uses a heuristic approach to determine
if it needs to join to the table, or if it can load all of the
records in a separate query. This method assumes it needs to join if it thinks
you need to query the columns from the other table, but sometimes
this method gets it wrong and executes a join even when not needed. In
this case using `preload` to explicitly load the data in a separate query
allows you to avoid the join, while still avoiding the N+1 query.
You can see a real example of this solution being used in
<https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655>.
#### De-normalize some foreign key to the table #### De-normalize some foreign key to the table
De-normalization refers to adding redundant precomputed (duplicated) data to De-normalization refers to adding redundant precomputed (duplicated) data to
...@@ -243,3 +257,37 @@ A quick checklist for fixing a specific join query would be: ...@@ -243,3 +257,37 @@ A quick checklist for fixing a specific join query would be:
adding a new column adding a new column
1. Can we remove the join by adding a new table in the correct database that 1. Can we remove the join by adding a new table in the correct database that
replicates the minimum data needed to do the join replicates the minimum data needed to do the join
#### How to validate you have correctly removed a cross-join
Using RSpec tests, you can validate all SQL queries within a code block to
ensure that none of them are joining across the two databases. This is a useful
tool to confirm you have correctly fixed an existing cross-join.
At some point in the future we will have fixed all cross-joins and this tool
will run by default in all tests. For now, the tool needs to be explicitly enabled
for your test.
You can use this method like so:
```ruby
it 'does not join across databases' do
with_cross_joins_prevented do
::Ci::Build.joins(:project).to_a
end
end
```
This will raise an exception if the query joins across the two databases. The
previous example is fixed by removing the join, like so:
```ruby
it 'does not join across databases' do
with_cross_joins_prevented do
::Ci::Build.preload(:project).to_a
end
end
```
You can see a real example of using this method for fixing a cross-join in
<https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655>.
...@@ -174,18 +174,18 @@ the operation you want to perform in each commit. To do so, you need to edit ...@@ -174,18 +174,18 @@ the operation you want to perform in each commit. To do so, you need to edit
the commits in your terminal's text editor. the commits in your terminal's text editor.
For example, if you're using [Vim](https://www.vim.org/) as the text editor in For example, if you're using [Vim](https://www.vim.org/) as the text editor in
a macOS's `ZSH` shell, and you want to **squash** all the three commits a macOS's `ZSH` shell, and you want to `squash` or `fixup` all the three commits
(join them into one): (join them into one):
1. Press <!-- vale gitlab.FirstPerson = NO --> <kbd>i</kbd> <!-- vale gitlab.FirstPerson = YES --> 1. Press <!-- vale gitlab.FirstPerson = NO --> <kbd>i</kbd> <!-- vale gitlab.FirstPerson = YES -->
on your keyboard to switch to Vim's editing mode. on your keyboard to switch to Vim's editing mode.
1. Navigate with your keyboard arrows to edit the **second** commit keyword 1. Navigate with your keyboard arrows to edit the **second** commit keyword
from `pick` to `squash` (or `s`). Do the same to the **third** commit. from `pick` to `squash` or `fixup` (or `s` or `f`). Do the same to the **third** commit.
The first commit should be left **unchanged** (`pick`) as we want to squash The first commit should be left **unchanged** (`pick`) as we want to squash
the second and third into the first. the second and third into the first.
1. Press <kbd>Escape</kbd> to leave the editing mode. 1. Press <kbd>Escape</kbd> to leave the editing mode.
1. Type `:wq` to "write" (save) and "quit". 1. Type `:wq` to "write" (save) and "quit".
1. Git outputs the commit message so you have a chance to edit it: 1. When squashing, Git outputs the commit message so you have a chance to edit it:
- All lines starting with `#` are ignored and not included in the commit - All lines starting with `#` are ignored and not included in the commit
message. Everything else is included. message. Everything else is included.
- To leave it as it is, type `:wq`. To edit the commit message: switch to the - To leave it as it is, type `:wq`. To edit the commit message: switch to the
......
...@@ -13,6 +13,8 @@ Git repositories become larger over time. When large files are added to a Git re ...@@ -13,6 +13,8 @@ Git repositories become larger over time. When large files are added to a Git re
- They take up a large amount of storage space on the server. - They take up a large amount of storage space on the server.
- Git repository storage limits [can be reached](#storage-limits). - Git repository storage limits [can be reached](#storage-limits).
Such problems can be detected with [git-sizer](https://github.com/github/git-sizer#getting-started).
Rewriting a repository can remove unwanted history to make the repository smaller. Rewriting a repository can remove unwanted history to make the repository smaller.
We **recommend [`git filter-repo`](https://github.com/newren/git-filter-repo/blob/main/README.md)** We **recommend [`git filter-repo`](https://github.com/newren/git-filter-repo/blob/main/README.md)**
over [`git filter-branch`](https://git-scm.com/docs/git-filter-branch) and over [`git filter-branch`](https://git-scm.com/docs/git-filter-branch) and
......
...@@ -210,3 +210,59 @@ To help avoid abuse, by default, users are rate limited to: ...@@ -210,3 +210,59 @@ To help avoid abuse, by default, users are rate limited to:
| Import | 6 projects per minute | | Import | 6 projects per minute |
GitLab.com may have [different settings](../../gitlab_com/index.md#importexport) from the defaults. GitLab.com may have [different settings](../../gitlab_com/index.md#importexport) from the defaults.
## Troubleshooting
### Import workaround for large repositories
[Maximum import size limitations](#importing-the-project)
can prevent an import from being successful.
If changing the import limits is not possible,
the following local workflow can be used to temporarily
reduce the repository size for another import attempt.
1. Create a temporary working directory from the export:
```shell
EXPORT=<filename-without-extension>
mkdir "$EXPORT"
tar -xf "$EXPORT".tar.gz --directory="$EXPORT"/
cd "$EXPORT"/
git clone project.bundle
# Prevent interference with recreating an importable file later
mv project.bundle ../"$EXPORT"-original.bundle
mv ../"$EXPORT".tar.gz ../"$EXPORT"-original.tar.gz
```
1. To reduce the repository size,
[identify and remove large files](../repository/reducing_the_repo_size_using_git.md)
or [interactively rebase and fixup](../../../topics/git/git_rebase.md#interactive-rebase)
to reduce the number of commits.
```shell
# Reduce the .git/objects/pack/ file size
cd project
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Prepare recreating an importable file
git bundle create ../project.bundle <default-branch-name>
cd ..
mv project/ ../"$EXPORT"-project
cd ..
# Recreate an importable file
tar -czf "$EXPORT"-smaller.tar.gz --directory="$EXPORT"/ .
```
1. Import this new, smaller file into GitLab.
1. In a full clone of the original repository,
use `git remote set-url origin <new-url> && git push --force --all`
to complete the import.
1. Update the imported repository's
[branch protection rules](../protected_branches.md) and
its [default branch](../repository/branches/default.md), and
delete the temporary, `smaller-…` branch, and
the local, temporary data.
...@@ -254,6 +254,7 @@ export default { ...@@ -254,6 +254,7 @@ export default {
<item-due-date <item-due-date
v-if="item.dueDate" v-if="item.dueDate"
:date="item.dueDate" :date="item.dueDate"
:closed="Boolean(item.closedAt)"
tooltip-placement="top" tooltip-placement="top"
css-class="item-due-date gl-display-flex gl-align-items-center gl-mr-5!" css-class="item-due-date gl-display-flex gl-align-items-center gl-mr-5!"
/> />
......
<script> <script>
import { mapActions } from 'vuex';
import NoEnvironmentEmptyState from '../no_environment_empty_state.vue';
import PoliciesHeader from './policies_header.vue'; import PoliciesHeader from './policies_header.vue';
import PoliciesList from './policies_list.vue';
export default { export default {
components: { components: {
PoliciesHeader, PoliciesHeader,
PoliciesList,
NoEnvironmentEmptyState,
},
inject: ['defaultEnvironmentId'],
data() {
return {
// We require the project to have at least one available environment.
// An invalid default environment id means there there are no available
// environments, therefore infrastructure cannot be set up. A valid default
// environment id only means that infrastructure *might* be set up.
shouldFetchEnvironment: this.isValidEnvironmentId(this.defaultEnvironmentId),
};
},
created() {
if (this.shouldFetchEnvironment) {
this.setCurrentEnvironmentId(this.defaultEnvironmentId);
this.fetchEnvironments();
}
},
methods: {
...mapActions('threatMonitoring', ['fetchEnvironments', 'setCurrentEnvironmentId']),
isValidEnvironmentId(id) {
return Number.isInteger(id) && id >= 0;
},
}, },
}; };
</script> </script>
<template> <template>
<policies-header /> <div>
<policies-header />
<no-environment-empty-state v-if="!shouldFetchEnvironment" />
<policies-list v-else />
</div>
</template> </template>
<script>
import {
GlTable,
GlEmptyState,
GlAlert,
GlSprintf,
GlLink,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { PREDEFINED_NETWORK_POLICIES } from 'ee/threat_monitoring/constants';
import createFlash from '~/flash';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { setUrlFragment, mergeUrlParams } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import networkPoliciesQuery from '../../graphql/queries/network_policies.query.graphql';
import scanExecutionPoliciesQuery from '../../graphql/queries/scan_execution_policies.query.graphql';
import { POLICY_TYPE_OPTIONS } from '../constants';
import EnvironmentPicker from '../environment_picker.vue';
import PolicyDrawer from '../policy_drawer/policy_drawer.vue';
import PolicyEnvironments from '../policy_environments.vue';
import PolicyTypeFilter from '../policy_type_filter.vue';
const createPolicyFetchError = ({ gqlError, networkError }) => {
const error =
gqlError?.message ||
networkError?.message ||
s__('NetworkPolicies|Something went wrong, unable to fetch policies');
createFlash({
message: error,
});
};
const getPoliciesWithType = (policies, policyType) =>
policies.map((policy) => ({
...policy,
policyType,
}));
export default {
components: {
GlTable,
GlEmptyState,
GlAlert,
GlSprintf,
GlLink,
GlIcon,
EnvironmentPicker,
PolicyTypeFilter,
PolicyDrawer,
PolicyEnvironments,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['projectPath', 'documentationPath', 'newPolicyPath'],
apollo: {
networkPolicies: {
query: networkPoliciesQuery,
variables() {
return {
fullPath: this.projectPath,
environmentId: this.allEnvironments ? null : this.currentEnvironmentGid,
};
},
update(data) {
const policies = data?.project?.networkPolicies?.nodes ?? [];
const predefined = PREDEFINED_NETWORK_POLICIES.filter(
({ name }) => !policies.some((policy) => name === policy.name),
);
return [...policies, ...predefined];
},
error: createPolicyFetchError,
skip() {
return this.isLoadingEnvironments || !this.shouldShowNetworkPolicies;
},
},
scanExecutionPolicies: {
query: scanExecutionPoliciesQuery,
variables() {
return {
fullPath: this.projectPath,
};
},
update(data) {
return data?.project?.scanExecutionPolicies?.nodes ?? [];
},
error: createPolicyFetchError,
},
},
data() {
return {
selectedPolicy: null,
networkPolicies: [],
scanExecutionPolicies: [],
selectedPolicyType: POLICY_TYPE_OPTIONS.ALL.value,
};
},
computed: {
...mapState('threatMonitoring', [
'currentEnvironmentId',
'allEnvironments',
'isLoadingEnvironments',
]),
...mapGetters('threatMonitoring', ['currentEnvironmentGid']),
allPolicyTypes() {
return {
[POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK.value]: this.networkPolicies,
[POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_EXECUTION.value]: this.scanExecutionPolicies,
};
},
documentationFullPath() {
return setUrlFragment(this.documentationPath, 'container-network-policy');
},
shouldShowNetworkPolicies() {
return [
POLICY_TYPE_OPTIONS.ALL.value,
POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK.value,
].includes(this.selectedPolicyType);
},
policies() {
const policyTypes =
this.selectedPolicyType === POLICY_TYPE_OPTIONS.ALL.value
? Object.keys(this.allPolicyTypes)
: [this.selectedPolicyType];
const policies = policyTypes.map((type) =>
getPoliciesWithType(this.allPolicyTypes[type], POLICY_TYPE_OPTIONS[type].text),
);
return policies.flat();
},
isLoadingPolicies() {
return (
this.isLoadingEnvironments ||
this.$apollo.queries.networkPolicies.loading ||
this.$apollo.queries.scanExecutionPolicies.loading
);
},
hasSelectedPolicy() {
return Boolean(this.selectedPolicy);
},
hasAutoDevopsPolicy() {
return Boolean(this.networkPolicies?.some((policy) => policy.fromAutoDevops));
},
editPolicyPath() {
return this.hasSelectedPolicy
? mergeUrlParams(
!this.selectedPolicy.kind
? { environment_id: this.currentEnvironmentId }
: { environment_id: this.currentEnvironmentId, kind: this.selectedPolicy.kind },
this.newPolicyPath.replace('new', `${this.selectedPolicy.name}/edit`),
)
: '';
},
fields() {
const environments = {
key: 'environments',
label: s__('SecurityPolicies|Environment(s)'),
};
const fields = [
{
key: 'status',
label: '',
thClass: 'gl-w-3',
tdAttr: {
'data-testid': 'policy-status-cell',
},
},
{
key: 'name',
label: __('Name'),
thClass: 'gl-w-half',
},
{
key: 'policyType',
label: s__('SecurityPolicies|Policy type'),
sortable: true,
},
{
key: 'updatedAt',
label: __('Last modified'),
sortable: true,
},
];
// Adds column 'environments' only while 'all environments' option is selected
if (this.allEnvironments) fields.splice(2, 0, environments);
return fields;
},
},
methods: {
getTimeAgoString(updatedAt) {
if (!updatedAt) return '';
return getTimeago().format(updatedAt);
},
presentPolicyDrawer(rows) {
if (rows.length === 0) return;
const [selectedPolicy] = rows;
this.selectedPolicy = selectedPolicy;
},
deselectPolicy() {
this.selectedPolicy = null;
const bTable = this.$refs.policiesTable.$children[0];
bTable.clearSelected();
},
},
i18n: {
emptyStateDescription: s__(
`NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other's network endpoints.`,
),
autodevopsNoticeDescription: s__(
`NetworkPolicies|If you are using Auto DevOps, your %{monospacedStart}auto-deploy-values.yaml%{monospacedEnd} file will not be updated if you change a policy in this section. Auto DevOps users should make changes by following the %{linkStart}Container Network Policy documentation%{linkEnd}.`,
),
emptyStateButton: __('Learn more'),
emptyStateTitle: s__('NetworkPolicies|No policies detected'),
statusEnabled: __('Enabled'),
statusDisabled: __('Disabled'),
},
};
</script>
<template>
<div>
<gl-alert
v-if="hasAutoDevopsPolicy"
data-testid="autodevopsAlert"
variant="info"
:dismissible="false"
class="gl-mb-3"
>
<gl-sprintf :message="$options.i18n.autodevopsNoticeDescription">
<template #monospaced="{ content }">
<span class="gl-font-monospace">{{ content }}</span>
</template>
<template #link="{ content }">
<gl-link :href="documentationFullPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<div class="gl-pt-5 gl-px-5 gl-bg-gray-10">
<div class="row gl-justify-content-space-between gl-align-items-center">
<div class="col-12 col-sm-8 col-md-6 col-lg-5 row">
<environment-picker data-testid="environment-picker" class="col-6" :include-all="true" />
<policy-type-filter
v-model="selectedPolicyType"
class="col-6"
data-testid="policy-type-filter"
/>
</div>
</div>
</div>
<gl-table
ref="policiesTable"
:busy="isLoadingPolicies"
:items="policies"
:fields="fields"
sort-icon-left
sort-by="updatedAt"
sort-desc
head-variant="white"
stacked="md"
thead-class="gl-text-gray-900 border-bottom"
tbody-class="gl-text-gray-900"
show-empty
hover
selectable
select-mode="single"
selected-variant="primary"
@row-selected="presentPolicyDrawer"
>
<template #cell(status)="value">
<gl-icon
v-if="value.item.enabled"
v-gl-tooltip="$options.i18n.statusEnabled"
:aria-label="$options.i18n.statusEnabled"
name="check-circle-filled"
class="gl-text-green-700"
/>
<span v-else class="gl-sr-only">{{ $options.i18n.statusDisabled }}</span>
</template>
<template #cell(environments)="value">
<policy-environments :environments="value.item.environments" />
</template>
<template #cell(updatedAt)="value">
{{ getTimeAgoString(value.item.updatedAt) }}
</template>
<template #empty>
<slot name="empty-state">
<gl-empty-state
ref="tableEmptyState"
:title="$options.i18n.emptyStateTitle"
:description="$options.i18n.emptyStateDescription"
:primary-button-link="documentationFullPath"
:primary-button-text="$options.i18n.emptyStateButton"
/>
</slot>
</template>
</gl-table>
<policy-drawer
:open="hasSelectedPolicy"
:policy="selectedPolicy"
:edit-policy-path="editPolicyPath"
data-testid="policyDrawer"
@close="deselectPolicy"
/>
</div>
</template>
...@@ -39,10 +39,8 @@ export default () => { ...@@ -39,10 +39,8 @@ export default () => {
} = el.dataset; } = el.dataset;
const store = createStore(); const store = createStore();
store.dispatch('threatMonitoring/setEndpoints', { store.dispatch('threatMonitoring/setStatisticsEndpoint', networkPolicyStatisticsEndpoint);
networkPolicyStatisticsEndpoint, store.dispatch('threatMonitoring/setEnvironmentEndpoint', environmentsEndpoint);
environmentsEndpoint,
});
return new Vue({ return new Vue({
apolloProvider, apolloProvider,
......
...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; ...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import SecurityPoliciesApp from './components/policies/policies_app.vue'; import SecurityPoliciesApp from './components/policies/policies_app.vue';
import createStore from './store';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -15,13 +16,20 @@ export default () => { ...@@ -15,13 +16,20 @@ export default () => {
const { const {
assignedPolicyProject, assignedPolicyProject,
disableSecurityPolicyProject, disableSecurityPolicyProject,
defaultEnvironmentId,
environmentsEndpoint,
emptyStateSvgPath,
documentationPath, documentationPath,
newPolicyPath, newPolicyPath,
projectPath, projectPath,
} = el.dataset; } = el.dataset;
const store = createStore();
store.dispatch('threatMonitoring/setEnvironmentEndpoint', environmentsEndpoint);
return new Vue({ return new Vue({
apolloProvider, apolloProvider,
store,
el, el,
provide: { provide: {
assignedPolicyProject: JSON.parse(assignedPolicyProject), assignedPolicyProject: JSON.parse(assignedPolicyProject),
...@@ -29,6 +37,8 @@ export default () => { ...@@ -29,6 +37,8 @@ export default () => {
documentationPath, documentationPath,
newPolicyPath, newPolicyPath,
projectPath, projectPath,
emptyStateSvgPath,
defaultEnvironmentId: parseInt(defaultEnvironmentId, 10),
}, },
render(createElement) { render(createElement) {
return createElement(SecurityPoliciesApp); return createElement(SecurityPoliciesApp);
......
...@@ -3,13 +3,12 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -3,13 +3,12 @@ import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setEndpoints = ({ commit }, endpoints) => { export const setEnvironmentEndpoint = ({ commit }, endpoint) => {
commit(types.SET_ENDPOINT, endpoints.environmentsEndpoint); commit(types.SET_ENDPOINT, endpoint);
commit( };
`threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
endpoints.networkPolicyStatisticsEndpoint, export const setStatisticsEndpoint = ({ commit }, endpoint) => {
{ root: true }, commit(`threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`, endpoint, { root: true });
);
}; };
export const requestEnvironments = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS); export const requestEnvironments = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS);
......
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module StageEventHash
extend ActiveSupport::Concern
prepended do
has_many :cycle_analytics_group_stages, class_name: 'Analytics::CycleAnalytics::GroupStage', inverse_of: :stage_event_hash
end
class_methods do
def unused_hashes_for(id)
exists_query = ::Analytics::CycleAnalytics::GroupStage.where(stage_event_hash_id: id).select('1').limit(1)
super.where.not('EXISTS (?)', exists_query)
end
end
end
end
end
end
- breadcrumb_title _("Policies") - breadcrumb_title _("Policies")
- disable_security_policy_project = !can_update_security_orchestration_policy_project?(project) - disable_security_policy_project = !can_update_security_orchestration_policy_project?(project)
- default_environment_id = project.default_environment&.id || -1
#js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json, #js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json,
default_environment_id: default_environment_id,
disable_security_policy_project: disable_security_policy_project.to_s, disable_security_policy_project: disable_security_policy_project.to_s,
documentation_path: help_page_path('user/application_security/policies/index.md'), documentation_path: help_page_path('user/application_security/policies/index.md'),
empty_state_svg_path: image_path('illustrations/monitoring/unable_to_connect.svg'),
new_policy_path: new_project_threat_monitoring_policy_path(project), new_policy_path: new_project_threat_monitoring_policy_path(project),
environments_endpoint: project_environments_path(project),
project_path: project.full_path } } project_path: project.full_path } }
...@@ -9,10 +9,10 @@ ...@@ -9,10 +9,10 @@
%p.gl-text-center %p.gl-text-center
- if remove_known_trial_form_fields_variant == :welcoming - if remove_known_trial_form_fields_variant == :welcoming
- salutation = current_user.first_name.present? ? " #{current_user.first_name}" : '' - salutation = current_user.first_name.present? ? " #{current_user.first_name}" : ''
- company = current_user.organization.present? ? current_user.organization : _('your company') - company = current_user.organization.present? ? current_user.organization : s_('Trial|your company')
= _("Hi%{salutation}, your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information about %{company} to activate your trial.") % { salutation: salutation, company: company } = s_("Trial|Hi%{salutation}, your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information about %{company} to activate your trial.") % { salutation: salutation, company: company }
- else - else
= _('Your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information to activate your trial.') = s_('Trial|Your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information to activate your trial.')
= render 'errors' = render 'errors'
...@@ -21,36 +21,36 @@ ...@@ -21,36 +21,36 @@
= hidden_field_tag :first_name, current_user.first_name = hidden_field_tag :first_name, current_user.first_name
- else - else
.form-group .form-group
= label_tag :first_name, _('First name'), for: :first_name, class: 'col-form-label' = label_tag :first_name, s_('Trial|First name'), for: :first_name, class: 'col-form-label'
- readonly = remove_known_trial_form_fields_variant == :noneditable && current_user.first_name.present? - readonly = remove_known_trial_form_fields_variant == :noneditable && current_user.first_name.present?
= text_field_tag :first_name, params[:first_name] || current_user.first_name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'first_name' }, readonly: readonly = text_field_tag :first_name, params[:first_name] || current_user.first_name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'first_name' }, readonly: readonly
- if remove_known_trial_form_fields_variant == :welcoming && current_user.last_name.present? - if remove_known_trial_form_fields_variant == :welcoming && current_user.last_name.present?
= hidden_field_tag :last_name, current_user.last_name = hidden_field_tag :last_name, current_user.last_name
- else - else
.form-group .form-group
= label_tag :last_name, _('Last name'), for: :last_name, class: 'col-form-label' = label_tag :last_name, s_('Trial|Last name'), for: :last_name, class: 'col-form-label'
- readonly = remove_known_trial_form_fields_variant == :noneditable && current_user.last_name.present? - readonly = remove_known_trial_form_fields_variant == :noneditable && current_user.last_name.present?
= text_field_tag :last_name, params[:last_name] || current_user.last_name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'last_name' }, readonly: readonly = text_field_tag :last_name, params[:last_name] || current_user.last_name, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'last_name' }, readonly: readonly
- if remove_known_trial_form_fields_variant == :welcoming && current_user.organization.present? - if remove_known_trial_form_fields_variant == :welcoming && current_user.organization.present?
= hidden_field_tag :company_name, current_user.organization = hidden_field_tag :company_name, current_user.organization
- else - else
.form-group .form-group
= label_tag :company_name, _('Company name'), for: :company_name, class: 'col-form-label' = label_tag :company_name, s_('Trial|Company name'), for: :company_name, class: 'col-form-label'
- readonly = remove_known_trial_form_fields_variant == :noneditable && current_user.organization.present? - readonly = remove_known_trial_form_fields_variant == :noneditable && current_user.organization.present?
= text_field_tag :company_name, params[:company_name] || current_user.organization, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'company_name' }, readonly: readonly = text_field_tag :company_name, params[:company_name] || current_user.organization, class: 'form-control gl-form-input', required: true, data: { qa_selector: 'company_name' }, readonly: readonly
.form-group.gl-select2-html5-required-fix .form-group.gl-select2-html5-required-fix
= label_tag :company_size, _('Number of employees'), for: :company_size, class: 'col-form-label' = label_tag :company_size, s_('Trial|Number of employees'), for: :company_size, class: 'col-form-label'
= select_tag :company_size, company_size_options_for_select(params[:company_size]), class: 'select2', required: true, data: { qa_selector: 'number_of_employees' } = select_tag :company_size, company_size_options_for_select(params[:company_size]), class: 'select2', required: true, data: { qa_selector: 'number_of_employees' }
.form-group .form-group
= label_tag :phone_number, _('Telephone number'), for: :phone_number, class: 'col-form-label' = label_tag :phone_number, s_('Trial|Telephone number'), for: :phone_number, class: 'col-form-label'
= telephone_field_tag :phone_number, params[:phone_number], pattern: '^(\+)*[0-9-\s]+$', class: 'form-control gl-form-input', required: true, data: { qa_selector: 'telephone_number' } = telephone_field_tag :phone_number, params[:phone_number], pattern: '^(\+)*[0-9-\s]+$', class: 'form-control gl-form-input', required: true, data: { qa_selector: 'telephone_number' }
%p.gl-text-gray-500= _('Allowed characters: +, 0-9, -, and spaces.') %p.gl-text-gray-500= _('Allowed characters: +, 0-9, -, and spaces.')
.form-group .form-group
= label_tag :number_of_users, _('How many users will be evaluating the trial?'), for: :number_of_users, class: 'col-form-label' = label_tag :number_of_users, s_('Trial|How many users will be evaluating the trial?'), for: :number_of_users, class: 'col-form-label'
= number_field_tag :number_of_users, params[:number_of_users], class: 'form-control gl-form-input', required: true, min: 1, data: { qa_selector: 'number_of_users' } = number_field_tag :number_of_users, params[:number_of_users], class: 'form-control gl-form-input', required: true, min: 1, data: { qa_selector: 'number_of_users' }
.form-group.gl-select2-html5-required-fix .form-group.gl-select2-html5-required-fix
= label_tag :country, _('Country'), class: 'col-form-label' = label_tag :country, s_('Trial|Country'), class: 'col-form-label'
= select_tag :country, options_for_select([[_('Please select a country'), '']]), class: 'select2 gl-transparent-pixel', required: true, id: 'country_select', data: { countries_end_point: countries_path, selected_option: params[:country], qa_selector: 'country' } = select_tag :country, options_for_select([[s_('Trial|Please select a country'), '']]), class: 'select2 gl-transparent-pixel', required: true, id: 'country_select', data: { countries_end_point: countries_path, selected_option: params[:country], qa_selector: 'country' }
= submit_tag _('Continue'), class: 'btn gl-button btn-confirm btn-block', data: { qa_selector: 'continue' } = submit_tag s_('Trial|Continue'), class: 'btn gl-button btn-confirm btn-block', data: { qa_selector: 'continue' }
= render 'skip_trial' = render 'skip_trial'
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_ci_secrets_management_vault_build_created
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -15,6 +15,10 @@ milestone: "13.12" ...@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_creating_epic_boards
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -15,6 +15,10 @@ milestone: "13.12" ...@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_viewing_epic_boards
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -15,6 +15,10 @@ milestone: "13.12" ...@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_updating_epic_board_names
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -12,7 +12,13 @@ status: data_available ...@@ -12,7 +12,13 @@ status: data_available
milestone: "13.12" milestone: "13.12"
introduced_by_url: introduced_by_url:
time_frame: 28d time_frame: 28d
data_source: data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_creating_epic_boards
- g_project_management_users_viewing_epic_boards
- g_project_management_users_updating_epic_board_names
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_ci_secrets_management_vault_build_created
distribution: distribution:
- ee - ee
tier: [] tier: []
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- p_terraform_state_api_unique_users
distribution: distribution:
- ee - ee
tier: [] tier: []
......
...@@ -15,6 +15,10 @@ milestone: "13.12" ...@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_creating_epic_boards
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -14,6 +14,10 @@ milestone: "13.12" ...@@ -14,6 +14,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_viewing_epic_boards
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -14,6 +14,10 @@ milestone: "13.12" ...@@ -14,6 +14,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_updating_epic_board_names
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -12,7 +12,13 @@ status: data_available ...@@ -12,7 +12,13 @@ status: data_available
milestone: "13.12" milestone: "13.12"
introduced_by_url: introduced_by_url:
time_frame: 7d time_frame: 7d
data_source: data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_creating_epic_boards
- g_project_management_users_viewing_epic_boards
- g_project_management_users_updating_epic_board_names
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -13,6 +13,9 @@ module EE ...@@ -13,6 +13,9 @@ module EE
audit_changes(:merge_requests_author_approval, as: 'prevent merge request approval from authors', model: model) audit_changes(:merge_requests_author_approval, as: 'prevent merge request approval from authors', model: model)
audit_changes(:merge_requests_disable_committers_approval, as: 'prevent merge request approval from reviewers', model: model) audit_changes(:merge_requests_disable_committers_approval, as: 'prevent merge request approval from reviewers', model: model)
audit_changes(:reset_approvals_on_push, as: 'require new approvals when new commits are added to an MR', model: model)
audit_changes(:disable_overriding_approvers_per_merge_request, as: 'prevent users from modifying MR approval rules in merge requests', model: model)
audit_changes(:require_password_to_approve, as: 'require user password for approvals', model: model)
audit_project_feature_changes audit_project_feature_changes
audit_compliance_framework_changes audit_compliance_framework_changes
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { PipelineKeyOptions } from '~/pipelines/constants';
import { triggeredBy, triggered } from './mock_data'; import { triggeredBy, triggered } from './mock_data';
...@@ -15,6 +16,7 @@ describe('Pipelines Table', () => { ...@@ -15,6 +16,7 @@ describe('Pipelines Table', () => {
const defaultProps = { const defaultProps = {
pipelines: [], pipelines: [],
viewType: 'root', viewType: 'root',
pipelineKeyOption: PipelineKeyOptions[0],
}; };
const createMockPipeline = () => { const createMockPipeline = () => {
......
...@@ -361,6 +361,19 @@ describe('RelatedItemsTree', () => { ...@@ -361,6 +361,19 @@ describe('RelatedItemsTree', () => {
expect(dueDate.isVisible()).toBe(true); expect(dueDate.isVisible()).toBe(true);
}); });
it('does not render red icon for overdue issue that is closed', async () => {
wrapper.setProps({
item: {
...mockItem,
closedAt: '2018-12-01T00:00:00.00Z',
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find(ItemDueDate).props('closed')).toBe(true);
});
it('renders item weight when it has weight', () => { it('renders item weight when it has weight', () => {
const weight = wrapper.find(ItemWeight); const weight = wrapper.find(ItemWeight);
......
import NoEnvironmentEmptyState from 'ee/threat_monitoring/components/no_environment_empty_state.vue';
import PoliciesApp from 'ee/threat_monitoring/components/policies/policies_app.vue'; import PoliciesApp from 'ee/threat_monitoring/components/policies/policies_app.vue';
import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue'; import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue';
import PoliciesList from 'ee/threat_monitoring/components/policies/policies_list.vue';
import createStore from 'ee/threat_monitoring/store';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Policies App', () => { describe('Policies App', () => {
let wrapper; let wrapper;
let store;
let setCurrentEnvironmentIdSpy;
let fetchEnvironmentsSpy;
const findPoliciesHeader = () => wrapper.findComponent(PoliciesHeader); const findPoliciesHeader = () => wrapper.findComponent(PoliciesHeader);
const findPoliciesList = () => wrapper.findComponent(PoliciesList);
const findEmptyState = () => wrapper.findComponent(NoEnvironmentEmptyState);
beforeEach(() => { const createWrapper = ({ provide } = {}) => {
wrapper = shallowMountExtended(PoliciesApp); store = createStore();
});
setCurrentEnvironmentIdSpy = jest
.spyOn(PoliciesApp.methods, 'setCurrentEnvironmentId')
.mockImplementation(() => {});
fetchEnvironmentsSpy = jest
.spyOn(PoliciesApp.methods, 'fetchEnvironments')
.mockImplementation(() => {});
wrapper = shallowMountExtended(PoliciesApp, {
store,
provide: {
defaultEnvironmentId: -1,
...provide,
},
});
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('mounts the policies header component', () => { describe('when does have an environment enabled', () => {
expect(findPoliciesHeader().exists()).toBe(true); beforeEach(() => {
createWrapper({ provide: { defaultEnvironmentId: 22 } });
});
it('mounts the policies header component', () => {
expect(findPoliciesHeader().exists()).toBe(true);
});
it('mounts the policies list component', () => {
expect(findPoliciesList().exists()).toBe(true);
});
it('does not mount the empty state', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('fetches the environments when created', async () => {
expect(setCurrentEnvironmentIdSpy).toHaveBeenCalled();
expect(fetchEnvironmentsSpy).toHaveBeenCalled();
});
});
describe('when does not have an environment enabled', () => {
beforeEach(() => {
createWrapper();
});
it('mounts the policies header component', () => {
expect(findPoliciesHeader().exists()).toBe(true);
});
it('does not mount the policies list component', () => {
expect(findPoliciesList().exists()).toBe(false);
});
it('mounts the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('does not fetch the environments when created', () => {
expect(setCurrentEnvironmentIdSpy).not.toHaveBeenCalled();
expect(fetchEnvironmentsSpy).not.toHaveBeenCalled();
});
}); });
}); });
import { GlTable, GlDrawer } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import { merge } from 'lodash';
import VueApollo from 'vue-apollo';
import { POLICY_TYPE_OPTIONS } from 'ee/threat_monitoring/components/constants';
import PoliciesList from 'ee/threat_monitoring/components/policies/policies_list.vue';
import PolicyDrawer from 'ee/threat_monitoring/components/policy_drawer/policy_drawer.vue';
import networkPoliciesQuery from 'ee/threat_monitoring/graphql/queries/network_policies.query.graphql';
import scanExecutionPoliciesQuery from 'ee/threat_monitoring/graphql/queries/scan_execution_policies.query.graphql';
import createStore from 'ee/threat_monitoring/store';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { networkPolicies, scanExecutionPolicies } from '../../mocks/mock_apollo';
import {
mockNetworkPoliciesResponse,
mockScanExecutionPoliciesResponse,
} from '../../mocks/mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
const fullPath = 'project/path';
const environments = [
{
id: 2,
global_id: 'gid://gitlab/Environment/2',
},
];
const defaultRequestHandlers = {
networkPolicies: networkPolicies(mockNetworkPoliciesResponse),
scanExecutionPolicies: scanExecutionPolicies(mockScanExecutionPoliciesResponse),
};
const pendingHandler = jest.fn(() => new Promise(() => {}));
describe('PoliciesList component', () => {
let store;
let wrapper;
let requestHandlers;
const factory = (mountFn = mountExtended) => (options = {}) => {
store = createStore();
const { state, handlers, ...wrapperOptions } = options;
Object.assign(store.state.networkPolicies, {
...state,
});
store.state.threatMonitoring.environments = environments;
requestHandlers = {
...defaultRequestHandlers,
...handlers,
};
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = mountFn(
PoliciesList,
merge(
{
propsData: {
documentationPath: 'documentation_path',
newPolicyPath: '/policies/new',
},
store,
provide: {
documentationPath: 'path/to/docs',
newPolicyPath: 'path/to/policy',
projectPath: fullPath,
},
apolloProvider: createMockApollo([
[networkPoliciesQuery, requestHandlers.networkPolicies],
[scanExecutionPoliciesQuery, requestHandlers.scanExecutionPolicies],
]),
stubs: {
PolicyDrawer: stubComponent(PolicyDrawer, {
props: {
...PolicyDrawer.props,
...GlDrawer.props,
},
}),
},
localVue,
},
wrapperOptions,
),
);
};
const mountShallowWrapper = factory(shallowMountExtended);
const mountWrapper = factory();
const findPolicyTypeFilter = () => wrapper.findByTestId('policy-type-filter');
const findEnvironmentsPicker = () => wrapper.findByTestId('environment-picker');
const findPoliciesTable = () => wrapper.findComponent(GlTable);
const findPolicyStatusCells = () => wrapper.findAllByTestId('policy-status-cell');
const findPolicyDrawer = () => wrapper.findByTestId('policyDrawer');
const findAutodevopsAlert = () => wrapper.findByTestId('autodevopsAlert');
afterEach(() => {
wrapper.destroy();
});
describe('initial state', () => {
beforeEach(() => {
mountShallowWrapper({
handlers: {
networkPolicies: pendingHandler,
},
});
});
it('renders EnvironmentPicker', () => {
expect(findEnvironmentsPicker().exists()).toBe(true);
});
it('renders closed editor drawer', () => {
const editorDrawer = findPolicyDrawer();
expect(editorDrawer.exists()).toBe(true);
expect(editorDrawer.props('open')).toBe(false);
});
it('does not render autodevops alert', () => {
expect(findAutodevopsAlert().exists()).toBe(false);
});
it('fetches policies', () => {
expect(requestHandlers.networkPolicies).toHaveBeenCalledWith({
fullPath,
});
expect(requestHandlers.scanExecutionPolicies).toHaveBeenCalledWith({
fullPath,
});
});
it("sets table's loading state", () => {
expect(findPoliciesTable().attributes('busy')).toBe('true');
});
});
describe('given policies have been fetched', () => {
let rows;
beforeEach(async () => {
mountWrapper();
await waitForPromises();
rows = wrapper.findAll('tr');
});
it('fetches network policies on environment change', async () => {
store.dispatch.mockReset();
await store.commit('threatMonitoring/SET_CURRENT_ENVIRONMENT_ID', 2);
expect(requestHandlers.networkPolicies).toHaveBeenCalledTimes(2);
expect(requestHandlers.networkPolicies.mock.calls[1][0]).toEqual({
fullPath: 'project/path',
environmentId: environments[0].global_id,
});
});
it('if network policies are filtered out, changing the environment does not trigger a fetch', async () => {
store.dispatch.mockReset();
expect(requestHandlers.networkPolicies).toHaveBeenCalledTimes(1);
findPolicyTypeFilter().vm.$emit(
'input',
POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_EXECUTION.value,
);
await store.commit('threatMonitoring/SET_CURRENT_ENVIRONMENT_ID', 2);
expect(requestHandlers.networkPolicies).toHaveBeenCalledTimes(1);
});
describe.each`
rowIndex | expectedPolicyName | expectedPolicyType
${1} | ${mockScanExecutionPoliciesResponse[0].name} | ${'Scan execution'}
${2} | ${mockNetworkPoliciesResponse[0].name} | ${'Network'}
${3} | ${'drop-outbound'} | ${'Network'}
${4} | ${'allow-inbound-http'} | ${'Network'}
`('policy in row #$rowIndex', ({ rowIndex, expectedPolicyName, expectedPolicyType }) => {
let row;
beforeEach(() => {
row = rows.at(rowIndex);
});
it(`renders ${expectedPolicyName} in the name cell`, () => {
expect(row.findAll('td').at(1).text()).toBe(expectedPolicyName);
});
it(`renders ${expectedPolicyType} in the policy type cell`, () => {
expect(row.findAll('td').at(2).text()).toBe(expectedPolicyType);
});
});
it.each`
description | filterBy | hiddenTypes
${'network'} | ${POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK} | ${[POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_EXECUTION]}
${'scan execution'} | ${POLICY_TYPE_OPTIONS.POLICY_TYPE_SCAN_EXECUTION} | ${[POLICY_TYPE_OPTIONS.POLICY_TYPE_NETWORK]}
`('policies filtered by $description type', async ({ filterBy, hiddenTypes }) => {
findPolicyTypeFilter().vm.$emit('input', filterBy.value);
await wrapper.vm.$nextTick();
expect(findPoliciesTable().text()).toContain(filterBy.text);
hiddenTypes.forEach((hiddenType) => {
expect(findPoliciesTable().text()).not.toContain(hiddenType.text);
});
});
});
describe('status column', () => {
beforeEach(() => {
mountWrapper();
});
it('renders a checkmark icon for enabled policies', () => {
const icon = findPolicyStatusCells().at(1).find('svg');
expect(icon.exists()).toBe(true);
expect(icon.props()).toMatchObject({
name: 'check-circle-filled',
ariaLabel: 'Enabled',
});
});
it('renders a "Disabled" label for screen readers for disabled policies', () => {
const span = findPolicyStatusCells().at(2).find('span');
expect(span.exists()).toBe(true);
expect(span.attributes('class')).toBe('gl-sr-only');
expect(span.text()).toBe('Disabled');
});
});
describe('with allEnvironments enabled', () => {
beforeEach(() => {
mountWrapper();
wrapper.vm.$store.state.threatMonitoring.allEnvironments = true;
});
it('renders environments column', () => {
const environmentsHeader = findPoliciesTable().findAll('[role="columnheader"]').at(2);
expect(environmentsHeader.text()).toContain('Environment(s)');
});
});
describe.each`
description | policy
${'network'} | ${mockNetworkPoliciesResponse[0]}
${'scan execution'} | ${mockScanExecutionPoliciesResponse[0]}
`('given there is a $description policy selected', ({ policy }) => {
beforeEach(() => {
mountShallowWrapper();
findPoliciesTable().vm.$emit('row-selected', [policy]);
});
it('renders opened editor drawer', () => {
const editorDrawer = findPolicyDrawer();
expect(editorDrawer.exists()).toBe(true);
expect(editorDrawer.props()).toMatchObject({
open: true,
policy,
});
});
});
describe('given an autodevops policy', () => {
beforeEach(() => {
const autoDevOpsPolicy = {
...mockNetworkPoliciesResponse[0],
name: 'auto-devops',
fromAutoDevops: true,
};
mountShallowWrapper({
handlers: {
networkPolicies: networkPolicies([autoDevOpsPolicy]),
},
});
});
it('renders autodevops alert', () => {
expect(findAutodevopsAlert().exists()).toBe(true);
});
});
});
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring/actions'; import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring/actions';
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types'; import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types';
import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state'; import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state';
...@@ -26,20 +25,13 @@ describe('Threat Monitoring actions', () => { ...@@ -26,20 +25,13 @@ describe('Threat Monitoring actions', () => {
createFlash.mockClear(); createFlash.mockClear();
}); });
describe('setEndpoints', () => { describe('threatMonitoring/setStatisticsEndpoint', () => {
it('commits the SET_ENDPOINT mutation', () => it('commits the SET_ENDPOINT mutation', () =>
testAction( testAction(
actions.setEndpoints, actions.setStatisticsEndpoint,
{ networkPolicyStatisticsEndpoint,
environmentsEndpoint,
networkPolicyStatisticsEndpoint,
},
state, state,
[ [
{
type: types.SET_ENDPOINT,
payload: environmentsEndpoint,
},
{ {
type: `threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`, type: `threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
payload: networkPolicyStatisticsEndpoint, payload: networkPolicyStatisticsEndpoint,
...@@ -49,6 +41,22 @@ describe('Threat Monitoring actions', () => { ...@@ -49,6 +41,22 @@ describe('Threat Monitoring actions', () => {
)); ));
}); });
describe('threatMonitoring/setEnvironmentEndpoint', () => {
it('commits the SET_ENDPOINT mutation', () =>
testAction(
actions.setEnvironmentEndpoint,
environmentsEndpoint,
state,
[
{
type: types.SET_ENDPOINT,
payload: environmentsEndpoint,
},
],
[],
));
});
describe('requestEnvironments', () => { describe('requestEnvironments', () => {
it('commits the REQUEST_ENVIRONMENTS mutation', () => it('commits the REQUEST_ENVIRONMENTS mutation', () =>
testAction( testAction(
......
...@@ -14,7 +14,10 @@ RSpec.describe EE::Audit::ProjectChangesAuditor do ...@@ -14,7 +14,10 @@ RSpec.describe EE::Audit::ProjectChangesAuditor do
repository_size_limit: 10, repository_size_limit: 10,
packages_enabled: true, packages_enabled: true,
merge_requests_author_approval: false, merge_requests_author_approval: false,
merge_requests_disable_committers_approval: true merge_requests_disable_committers_approval: true,
reset_approvals_on_push: false,
disable_overriding_approvers_per_merge_request: false,
require_password_to_approve: false
) )
end end
...@@ -162,6 +165,45 @@ RSpec.describe EE::Audit::ProjectChangesAuditor do ...@@ -162,6 +165,45 @@ RSpec.describe EE::Audit::ProjectChangesAuditor do
) )
end end
end end
it 'creates an event when the reset approvals on push changes' do
project.update!(reset_approvals_on_push: true)
aggregate_failures do
expect { foo_instance.execute }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details).to include(
change: 'require new approvals when new commits are added to an MR',
from: false,
to: true
)
end
end
it 'creates an event when the require password to approve changes' do
project.update!(require_password_to_approve: true)
aggregate_failures do
expect { foo_instance.execute }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details).to include(
change: 'require user password for approvals',
from: false,
to: true
)
end
end
it 'creates an event when the disable overriding approvers per merge request changes' do
project.update!(disable_overriding_approvers_per_merge_request: true)
aggregate_failures do
expect { foo_instance.execute }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details).to include(
change: 'prevent users from modifying MR approval rules in merge requests',
from: false,
to: true
)
end
end
end end
end end
end end
...@@ -54,6 +54,25 @@ RSpec.describe Gitlab::UsageDataMetrics do ...@@ -54,6 +54,25 @@ RSpec.describe Gitlab::UsageDataMetrics do
]) ])
end end
it 'includes i_ci_secrets_management_vault_build_created monthly and weekly keys' do
expect(subject[:redis_hll_counters][:ci_secrets_management].keys).to contain_exactly(*[
:i_ci_secrets_management_vault_build_created_monthly, :i_ci_secrets_management_vault_build_created_weekly
])
end
it 'includes epic_boards_usage monthly and weekly keys' do
expect(subject[:redis_hll_counters][:epic_boards_usage].keys).to contain_exactly(*[
:g_project_management_users_creating_epic_boards_monthly, :g_project_management_users_creating_epic_boards_weekly,
:g_project_management_users_viewing_epic_boards_monthly, :g_project_management_users_viewing_epic_boards_weekly,
:g_project_management_users_updating_epic_board_names_monthly, :g_project_management_users_updating_epic_board_names_weekly,
:epic_boards_usage_total_unique_counts_monthly, :epic_boards_usage_total_unique_counts_weekly
])
end
it 'includes terraform weekly key' do
expect(subject[:redis_hll_counters][:terraform].keys).to include(:p_terraform_state_api_unique_users_weekly)
end
it 'includes issues_edit monthly and weekly keys' do it 'includes issues_edit monthly and weekly keys' do
expect(subject[:redis_hll_counters][:issues_edit].keys).to include( expect(subject[:redis_hll_counters][:issues_edit].keys).to include(
:g_project_management_issue_iteration_changed_monthly, :g_project_management_issue_iteration_changed_weekly, :g_project_management_issue_iteration_changed_monthly, :g_project_management_issue_iteration_changed_weekly,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do
let(:stage_event_hash) { described_class.create!(hash_sha256: hash_sha256) }
let(:hash_sha256) { 'does_not_matter' }
describe 'associations' do
it { is_expected.to have_many(:cycle_analytics_group_stages) }
end
describe '.cleanup_if_unused' do
it 'removes the record if there is no project or group stages with given stage events hash' do
described_class.cleanup_if_unused(stage_event_hash.id)
expect(described_class.find_by_id(stage_event_hash.id)).to be_nil
end
it 'does not remove the record if at least 1 group stage for the given stage events hash exists' do
id = create(:cycle_analytics_group_stage).stage_event_hash_id
described_class.cleanup_if_unused(id)
expect(described_class.find_by_id(id)).not_to be_nil
end
end
end
...@@ -9,6 +9,8 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid ...@@ -9,6 +9,8 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
end end
class VulnerabilitiesFinding < ActiveRecord::Base class VulnerabilitiesFinding < ActiveRecord::Base
include ShaAttribute
self.table_name = "vulnerability_occurrences" self.table_name = "vulnerability_occurrences"
belongs_to :primary_identifier, class_name: 'VulnerabilitiesIdentifier', inverse_of: :primary_findings, foreign_key: 'primary_identifier_id' belongs_to :primary_identifier, class_name: 'VulnerabilitiesIdentifier', inverse_of: :primary_findings, foreign_key: 'primary_identifier_id'
REPORT_TYPES = { REPORT_TYPES = {
...@@ -21,6 +23,9 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid ...@@ -21,6 +23,9 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
api_fuzzing: 6 api_fuzzing: 6
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
enum report_type: REPORT_TYPES enum report_type: REPORT_TYPES
sha_attribute :fingerprint
sha_attribute :location_fingerprint
end end
class CalculateFindingUUID class CalculateFindingUUID
......
...@@ -71,7 +71,7 @@ module Gitlab ...@@ -71,7 +71,7 @@ module Gitlab
'continuous_delivery' 'continuous_delivery'
], ],
[ [
%r(#{RESERVED_WORDS_PREFIX}/environments\.json\z), %r(#{RESERVED_WORDS_PREFIX}/-/environments\.json\z),
'environments', 'environments',
'continuous_delivery' 'continuous_delivery'
], ],
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
].freeze ].freeze
HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [ HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [
SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError, EOFError, SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep
].freeze ].freeze
......
...@@ -8209,9 +8209,6 @@ msgstr "" ...@@ -8209,9 +8209,6 @@ msgstr ""
msgid "Company" msgid "Company"
msgstr "" msgstr ""
msgid "Company name"
msgstr ""
msgid "Compare" msgid "Compare"
msgstr "" msgstr ""
...@@ -9289,9 +9286,6 @@ msgstr "" ...@@ -9289,9 +9286,6 @@ msgstr ""
msgid "Couldn't assign policy to project" msgid "Couldn't assign policy to project"
msgstr "" msgstr ""
msgid "Country"
msgstr ""
msgid "Coverage" msgid "Coverage"
msgstr "" msgstr ""
...@@ -16370,9 +16364,6 @@ msgstr "" ...@@ -16370,9 +16364,6 @@ msgstr ""
msgid "Hi %{username}!" msgid "Hi %{username}!"
msgstr "" msgstr ""
msgid "Hi%{salutation}, your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information about %{company} to activate your trial."
msgstr ""
msgid "Hide" msgid "Hide"
msgstr "" msgstr ""
...@@ -16509,9 +16500,6 @@ msgstr "" ...@@ -16509,9 +16500,6 @@ msgstr ""
msgid "How many seconds an IP will be counted towards the limit" msgid "How many seconds an IP will be counted towards the limit"
msgstr "" msgstr ""
msgid "How many users will be evaluating the trial?"
msgstr ""
msgid "I accept the %{terms_link}" msgid "I accept the %{terms_link}"
msgstr "" msgstr ""
...@@ -22918,9 +22906,6 @@ msgstr "" ...@@ -22918,9 +22906,6 @@ msgstr ""
msgid "Number of commits per MR" msgid "Number of commits per MR"
msgstr "" msgstr ""
msgid "Number of employees"
msgstr ""
msgid "Number of events" msgid "Number of events"
msgstr "" msgstr ""
...@@ -24189,6 +24174,12 @@ msgstr "" ...@@ -24189,6 +24174,12 @@ msgstr ""
msgid "Pipeline %{label} for \"%{dataTitle}\"" msgid "Pipeline %{label} for \"%{dataTitle}\""
msgstr "" msgstr ""
msgid "Pipeline ID"
msgstr ""
msgid "Pipeline IID"
msgstr ""
msgid "Pipeline Schedule" msgid "Pipeline Schedule"
msgstr "" msgstr ""
...@@ -30450,6 +30441,12 @@ msgstr "" ...@@ -30450,6 +30441,12 @@ msgstr ""
msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{boldStart}will%{boldEnd} lose access to your account." msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{boldStart}will%{boldEnd} lose access to your account."
msgstr "" msgstr ""
msgid "Show Pipeline ID"
msgstr ""
msgid "Show Pipeline IID"
msgstr ""
msgid "Show all activity" msgid "Show all activity"
msgstr "" msgstr ""
...@@ -32449,9 +32446,6 @@ msgstr "" ...@@ -32449,9 +32446,6 @@ msgstr ""
msgid "TeamcityIntegration|Trigger TeamCity CI after every push to the repository, except branch delete" msgid "TeamcityIntegration|Trigger TeamCity CI after every push to the repository, except branch delete"
msgstr "" msgstr ""
msgid "Telephone number"
msgstr ""
msgid "Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}." msgid "Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}."
msgstr "" msgstr ""
...@@ -35011,6 +35005,9 @@ msgstr "" ...@@ -35011,6 +35005,9 @@ msgstr ""
msgid "Trial|Company name" msgid "Trial|Company name"
msgstr "" msgstr ""
msgid "Trial|Continue"
msgstr ""
msgid "Trial|Continue using the basic features of GitLab for free." msgid "Trial|Continue using the basic features of GitLab for free."
msgstr "" msgstr ""
...@@ -35020,15 +35017,30 @@ msgstr "" ...@@ -35020,15 +35017,30 @@ msgstr ""
msgid "Trial|Dismiss" msgid "Trial|Dismiss"
msgstr "" msgstr ""
msgid "Trial|First name"
msgstr ""
msgid "Trial|GitLab Ultimate trial (optional)" msgid "Trial|GitLab Ultimate trial (optional)"
msgstr "" msgstr ""
msgid "Trial|Hi%{salutation}, your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information about %{company} to activate your trial."
msgstr ""
msgid "Trial|How many employees will use Gitlab?" msgid "Trial|How many employees will use Gitlab?"
msgstr "" msgstr ""
msgid "Trial|How many users will be evaluating the trial?"
msgstr ""
msgid "Trial|Last name"
msgstr ""
msgid "Trial|Number of employees" msgid "Trial|Number of employees"
msgstr "" msgstr ""
msgid "Trial|Please select a country"
msgstr ""
msgid "Trial|Successful trial activation image" msgid "Trial|Successful trial activation image"
msgstr "" msgstr ""
...@@ -35041,6 +35053,12 @@ msgstr "" ...@@ -35041,6 +35053,12 @@ msgstr ""
msgid "Trial|We will activate your trial on your group after you complete this step. After 30 days, you can:" msgid "Trial|We will activate your trial on your group after you complete this step. After 30 days, you can:"
msgstr "" msgstr ""
msgid "Trial|Your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information to activate your trial."
msgstr ""
msgid "Trial|your company"
msgstr ""
msgid "Trigger" msgid "Trigger"
msgstr "" msgstr ""
...@@ -38233,9 +38251,6 @@ msgstr "" ...@@ -38233,9 +38251,6 @@ msgstr ""
msgid "Your GPG keys (%{count})" msgid "Your GPG keys (%{count})"
msgstr "" msgstr ""
msgid "Your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information to activate your trial."
msgstr ""
msgid "Your GitLab account has been locked due to an excessive amount of unsuccessful sign in attempts. Your account will automatically unlock in %{duration} or you may click the link below to unlock now." msgid "Your GitLab account has been locked due to an excessive amount of unsuccessful sign in attempts. Your account will automatically unlock in %{duration} or you may click the link below to unlock now."
msgstr "" msgstr ""
...@@ -40180,8 +40195,5 @@ msgstr "" ...@@ -40180,8 +40195,5 @@ msgstr ""
msgid "yaml invalid" msgid "yaml invalid"
msgstr "" msgstr ""
msgid "your company"
msgstr ""
msgid "your settings" msgid "your settings"
msgstr "" msgstr ""
...@@ -632,7 +632,9 @@ module QA ...@@ -632,7 +632,9 @@ module QA
module Helpers module Helpers
autoload :ContextSelector, 'qa/specs/helpers/context_selector' autoload :ContextSelector, 'qa/specs/helpers/context_selector'
autoload :ContextFormatter, 'qa/specs/helpers/context_formatter'
autoload :Quarantine, 'qa/specs/helpers/quarantine' autoload :Quarantine, 'qa/specs/helpers/quarantine'
autoload :QuarantineFormatter, 'qa/specs/helpers/quarantine_formatter'
autoload :RSpec, 'qa/specs/helpers/rspec' autoload :RSpec, 'qa/specs/helpers/rspec'
end end
end end
...@@ -680,6 +682,7 @@ module QA ...@@ -680,6 +682,7 @@ module QA
autoload :WaitForRequests, 'qa/support/wait_for_requests' autoload :WaitForRequests, 'qa/support/wait_for_requests'
autoload :OTP, 'qa/support/otp' autoload :OTP, 'qa/support/otp'
autoload :SSH, 'qa/support/ssh' autoload :SSH, 'qa/support/ssh'
autoload :AllureMetadataFormatter, 'qa/support/allure_metadata_formatter.rb'
end end
end end
......
...@@ -67,25 +67,8 @@ module QA ...@@ -67,25 +67,8 @@ module QA
# @return [void] # @return [void]
def configure_rspec def configure_rspec
RSpec.configure do |config| RSpec.configure do |config|
config.formatter = AllureRspecFormatter config.add_formatter(AllureRspecFormatter)
config.add_formatter(QA::Support::AllureMetadataFormatter)
config.after do |example|
next if example.attempts && example.attempts > 0
testcase = example.metadata[:testcase]
example.tms('Testcase', testcase) if testcase
quarantine_issue = example.metadata.dig(:quarantine, :issue)
example.issue('Quarantine issue', quarantine_issue) if quarantine_issue
spec_file = example.file_path.split('/').last
example.issue(
'Failure issues',
"https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}"
)
example.add_link(name: "Job(#{Env.ci_job_name})", url: Env.ci_job_url) if Env.running_in_ci?
end
end end
end end
......
...@@ -96,9 +96,6 @@ module QA ...@@ -96,9 +96,6 @@ module QA
end end
after do |example| after do |example|
# skip saving data if example is skipped or failed before import finished
next if example.pending?
user.remove_via_api! user.remove_via_api!
next unless defined?(@import_time) next unless defined?(@import_time)
......
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
# This test was quarantined because relative URL isn't supported RSpec.describe(
# See https://gitlab.com/gitlab-org/gitlab/issues/13833 'Create',
RSpec.describe 'Create', :runner, :quarantine do :runner,
quarantine: {
only: { job: 'relative_url' },
issue: 'https://gitlab.com/gitlab-org/gitlab/issues/13833',
type: :bug
}
) do
describe 'Web IDE web terminal' do describe 'Web IDE web terminal' do
before do before do
project = Resource::Project.fabricate_via_api! do |project| project = Resource::Project.fabricate_via_api! do |project|
......
# frozen_string_literal: true
require 'rspec/core'
require "rspec/core/formatters/base_formatter"
module QA
module Specs
module Helpers
class ContextFormatter < ::RSpec::Core::Formatters::BaseFormatter
include ContextSelector
::RSpec::Core::Formatters.register(
self,
:example_group_started,
:example_started
)
# Starts example group
# @param [RSpec::Core::Notifications::GroupNotification] example_group_notification
# @return [void]
def example_group_started(example_group_notification)
set_skip_metadata(example_group_notification.group)
end
# Starts example
# @param [RSpec::Core::Notifications::ExampleNotification] example_notification
# @return [void]
def example_started(example_notification)
example = example_notification.example
# if skip propagated from example_group, do not reset skip metadata
set_skip_metadata(example_notification.example) unless example.metadata[:skip]
end
private
# Skip example_group or example
#
# @param [<RSpec::Core::ExampleGroup, RSpec::Core::Example>] example
# @return [void]
def set_skip_metadata(example)
return skip_only(example.metadata) if example.metadata.key?(:only)
return skip_except(example.metadata) if example.metadata.key?(:except)
end
# Skip based on 'only' condition
#
# @param [Hash] metadata
# @return [void]
def skip_only(metadata)
return if context_matches?(metadata[:only])
metadata[:skip] = 'Test is not compatible with this environment or pipeline'
end
# Skip based on 'except' condition
#
# @param [Hash] metadata
# @return [void]
def skip_except(metadata)
return unless except?(metadata[:except])
metadata[:skip] = 'Test is excluded in this job'
end
end
end
end
end
...@@ -8,18 +8,6 @@ module QA ...@@ -8,18 +8,6 @@ module QA
module ContextSelector module ContextSelector
extend self extend self
def configure_rspec
::RSpec.configure do |config|
config.before do |example|
if example.metadata.key?(:only)
skip('Test is not compatible with this environment or pipeline') unless ContextSelector.context_matches?(example.metadata[:only])
elsif example.metadata.key?(:except)
skip('Test is excluded in this job') if ContextSelector.except?(example.metadata[:except])
end
end
end
end
def except?(*options) def except?(*options)
return false if Runtime::Env.ci_job_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:job].present? } return false if Runtime::Env.ci_job_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:job].present? }
return false if Runtime::Env.ci_project_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:pipeline].present? } return false if Runtime::Env.ci_project_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:pipeline].present? }
......
...@@ -10,26 +10,6 @@ module QA ...@@ -10,26 +10,6 @@ module QA
extend self extend self
def configure_rspec
::RSpec.configure do |config|
config.before(:context, :quarantine) do
Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class)
end
config.before do |example|
Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example)
end
end
end
# Skip the entire context if a context is quarantined. This avoids running
# before blocks unnecessarily.
def skip_or_run_quarantined_contexts(filters, example)
return unless example.metadata.key?(:quarantine)
skip_or_run_quarantined_tests_or_contexts(filters, example)
end
# Skip tests in quarantine unless we explicitly focus on them. # Skip tests in quarantine unless we explicitly focus on them.
def skip_or_run_quarantined_tests_or_contexts(filters, example) def skip_or_run_quarantined_tests_or_contexts(filters, example)
if filters.key?(:quarantine) if filters.key?(:quarantine)
...@@ -43,19 +23,19 @@ module QA ...@@ -43,19 +23,19 @@ module QA
# running that ldap test as well because of the :quarantine metadata. # running that ldap test as well because of the :quarantine metadata.
# We could use an exclusion filter, but this way the test report will list # We could use an exclusion filter, but this way the test report will list
# the quarantined tests when they're not run so that we're aware of them # the quarantined tests when they're not run so that we're aware of them
skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters) if should_skip_when_focused?(example.metadata, included_filters)
else example.metadata[:skip] = "Only running tests tagged with :quarantine and any of #{included_filters.keys}"
if example.metadata.key?(:quarantine) end
quarantine_tag = example.metadata[:quarantine] elsif example.metadata.key?(:quarantine)
quarantine_tag = example.metadata[:quarantine]
if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only)
# If the :quarantine hash contains :only, we respect that.
# For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging.
return unless ContextSelector.context_matches?(quarantine_tag[:only])
end
skip(quarantine_message(quarantine_tag)) if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only) && !ContextSelector.context_matches?(quarantine_tag[:only])
# If the :quarantine hash contains :only, we respect that.
# For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging.
return
end end
example.metadata[:skip] = quarantine_message(quarantine_tag)
end end
end end
...@@ -64,7 +44,7 @@ module QA ...@@ -64,7 +44,7 @@ module QA
end end
def quarantine_message(quarantine_tag) def quarantine_message(quarantine_tag)
quarantine_message = %w(In quarantine) quarantine_message = %w[In quarantine]
quarantine_message << case quarantine_tag quarantine_message << case quarantine_tag
when String when String
": #{quarantine_tag}" ": #{quarantine_tag}"
......
# frozen_string_literal: true
require 'rspec/core'
require "rspec/core/formatters/base_formatter"
module QA
module Specs
module Helpers
class QuarantineFormatter < ::RSpec::Core::Formatters::BaseFormatter
include Quarantine
::RSpec::Core::Formatters.register(
self,
:example_group_started,
:example_started
)
# Starts example group
# @param [RSpec::Core::Notifications::GroupNotification] example_group_notification
# @return [void]
def example_group_started(example_group_notification)
group = example_group_notification.group
skip_or_run_quarantined_tests_or_contexts(filters, group)
end
# Starts example
# @param [RSpec::Core::Notifications::ExampleNotification] example_notification
# @return [void]
def example_started(example_notification)
example = example_notification.example
# if skip propagated from example_group, do not reset skip metadata
skip_or_run_quarantined_tests_or_contexts(filters, example) unless example.metadata[:skip]
end
private
def filters
@filters ||= ::RSpec.configuration.inclusion_filter.rules
end
end
end
end
end
...@@ -19,8 +19,10 @@ module QA ...@@ -19,8 +19,10 @@ module QA
# expanding into the global state # expanding into the global state
# See: https://github.com/rspec/rspec-core/issues/2603 # See: https://github.com/rspec/rspec-core/issues/2603
def describe_successfully(*args, &describe_body) def describe_successfully(*args, &describe_body)
reporter = ::RSpec.configuration.reporter
example_group = RSpec.describe(*args, &describe_body) example_group = RSpec.describe(*args, &describe_body)
ran_successfully = example_group.run RaiseOnFailuresReporter ran_successfully = example_group.run reporter
expect(ran_successfully).to eq true expect(ran_successfully).to eq true
example_group example_group
end end
......
# frozen_string_literal: true
require 'rspec/core'
require "rspec/core/formatters/base_formatter"
module QA
module Support
class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter
::RSpec::Core::Formatters.register(
self,
:example_started
)
# Starts example
# @param [RSpec::Core::Notifications::ExampleNotification] example_notification
# @return [void]
def example_started(example_notification)
example = example_notification.example
testcase = example.metadata[:testcase]
example.tms('Testcase', testcase) if testcase
quarantine_issue = example.metadata.dig(:quarantine, :issue)
example.issue('Quarantine issue', quarantine_issue) if quarantine_issue
spec_file = example.file_path.split('/').last
example.issue(
'Failure issues',
"https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}"
)
return unless Runtime::Env.running_in_ci?
example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url)
end
end
end
end
...@@ -26,8 +26,8 @@ Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| requir ...@@ -26,8 +26,8 @@ Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| requir
RSpec.configure do |config| RSpec.configure do |config|
config.include ::Matchers config.include ::Matchers
QA::Specs::Helpers::Quarantine.configure_rspec config.add_formatter QA::Specs::Helpers::ContextFormatter
QA::Specs::Helpers::ContextSelector.configure_rspec config.add_formatter QA::Specs::Helpers::QuarantineFormatter
config.before do |example| config.before do |example|
QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n")
......
...@@ -5,21 +5,7 @@ require 'allure-rspec' ...@@ -5,21 +5,7 @@ require 'allure-rspec'
describe QA::Runtime::AllureReport do describe QA::Runtime::AllureReport do
include Helpers::StubENV include Helpers::StubENV
let(:rspec_config) { double('RSpec::Core::Configuration', 'formatter=': nil, after: nil) } let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, after: nil) }
let(:rspec_example) do
double(
'RSpec::Core::Example',
tms: nil,
issue: nil,
add_link: nil,
attempts: 0,
file_path: 'file/path/spec.rb',
metadata: {
testcase: 'testcase',
quarantine: { issue: 'issue' }
}
)
end
let(:png_path) { 'png_path' } let(:png_path) { 'png_path' }
let(:html_path) { 'html_path' } let(:html_path) { 'html_path' }
...@@ -36,7 +22,6 @@ describe QA::Runtime::AllureReport do ...@@ -36,7 +22,6 @@ describe QA::Runtime::AllureReport do
allow(AllureRspec).to receive(:configure).and_yield(allure_config) allow(AllureRspec).to receive(:configure).and_yield(allure_config)
allow(RSpec).to receive(:configure).and_yield(rspec_config) allow(RSpec).to receive(:configure).and_yield(rspec_config)
allow(rspec_config).to receive(:after).and_yield(rspec_example)
allow(Capybara::Screenshot).to receive(:after_save_screenshot).and_yield(png_path) allow(Capybara::Screenshot).to receive(:after_save_screenshot).and_yield(png_path)
allow(Capybara::Screenshot).to receive(:after_save_html).and_yield(html_path) allow(Capybara::Screenshot).to receive(:after_save_html).and_yield(html_path)
end end
...@@ -62,12 +47,10 @@ describe QA::Runtime::AllureReport do ...@@ -62,12 +47,10 @@ describe QA::Runtime::AllureReport do
let(:png_file) { 'png-file' } let(:png_file) { 'png-file' }
let(:html_file) { 'html-file' } let(:html_file) { 'html-file' }
let(:ci_job) { 'ee:relative 5' } let(:ci_job) { 'ee:relative 5' }
let(:ci_job_url) { 'url' }
before do before do
stub_env('CI', 'true') stub_env('CI', 'true')
stub_env('CI_JOB_NAME', ci_job) stub_env('CI_JOB_NAME', ci_job)
stub_env('CI_JOB_URL', ci_job_url)
allow(Allure).to receive(:add_attachment) allow(Allure).to receive(:add_attachment)
allow(File).to receive(:open).with(png_path) { png_file } allow(File).to receive(:open).with(png_path) { png_file }
...@@ -85,20 +68,9 @@ describe QA::Runtime::AllureReport do ...@@ -85,20 +68,9 @@ describe QA::Runtime::AllureReport do
end end
end end
it 'adds rspec formatter' do it 'adds rspec and metadata formatter' do
expect(rspec_config).to have_received(:formatter=).with(AllureRspecFormatter) expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered
end expect(rspec_config).to have_received(:add_formatter).with(QA::Support::AllureMetadataFormatter).ordered
it 'configures after block' do
aggregate_failures do
expect(rspec_example).to have_received(:tms).with('Testcase', 'testcase')
expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue')
expect(rspec_example).to have_received(:add_link).with(name: "Job(#{ci_job})", url: ci_job_url)
expect(rspec_example).to have_received(:issue).with(
'Failure issues',
'https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=spec.rb'
)
end
end end
it 'configures screenshot saving' do it 'configures screenshot saving' do
......
...@@ -2,29 +2,25 @@ ...@@ -2,29 +2,25 @@
require 'rspec/core/sandbox' require 'rspec/core/sandbox'
RSpec.configure do |c| RSpec.describe QA::Specs::Helpers::ContextSelector do
c.around do |ex| include Helpers::StubENV
include QA::Specs::Helpers::RSpec
around do |ex|
QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
RSpec::Core::Sandbox.sandboxed do |config| RSpec::Core::Sandbox.sandboxed do |config|
config.formatter = QA::Specs::Helpers::ContextFormatter
# If there is an example-within-an-example, we want to make sure the inner example # If there is an example-within-an-example, we want to make sure the inner example
# does not get a reference to the outer example (the real spec) if it calls # does not get a reference to the outer example (the real spec) if it calls
# something like `pending` # something like `pending`
config.before(:context) { RSpec.current_example = nil } config.before(:context) { RSpec.current_example = nil }
config.color_mode = :off config.color_mode = :off
ex.run ex.run
end end
end end
end
RSpec.describe QA::Specs::Helpers::ContextSelector do
include Helpers::StubENV
include QA::Specs::Helpers::RSpec
before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
described_class.configure_rspec
end
describe '.context_matches?' do describe '.context_matches?' do
it 'returns true when url has .com' do it 'returns true when url has .com' do
...@@ -104,7 +100,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do ...@@ -104,7 +100,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with different environment set' do context 'with different environment set' do
before do before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com') QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com')
described_class.configure_rspec
end end
it 'does not run against production' do it 'does not run against production' do
...@@ -239,7 +234,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do ...@@ -239,7 +234,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'without CI_PROJECT_NAME set' do context 'without CI_PROJECT_NAME set' do
before do before do
stub_env('CI_PROJECT_NAME', nil) stub_env('CI_PROJECT_NAME', nil)
described_class.configure_rspec
end end
it 'runs on any pipeline' do it 'runs on any pipeline' do
...@@ -273,7 +267,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do ...@@ -273,7 +267,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'when a pipeline triggered from the default branch runs in gitlab-qa' do context 'when a pipeline triggered from the default branch runs in gitlab-qa' do
before do before do
stub_env('CI_PROJECT_NAME', 'gitlab-qa') stub_env('CI_PROJECT_NAME', 'gitlab-qa')
described_class.configure_rspec
end end
it 'runs on default branch pipelines' do it 'runs on default branch pipelines' do
...@@ -310,7 +303,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do ...@@ -310,7 +303,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with CI_PROJECT_NAME set' do context 'with CI_PROJECT_NAME set' do
before do before do
stub_env('CI_PROJECT_NAME', 'NIGHTLY') stub_env('CI_PROJECT_NAME', 'NIGHTLY')
described_class.configure_rspec
end end
it 'runs on designated pipeline' do it 'runs on designated pipeline' do
...@@ -353,7 +345,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do ...@@ -353,7 +345,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'without CI_JOB_NAME set' do context 'without CI_JOB_NAME set' do
before do before do
stub_env('CI_JOB_NAME', nil) stub_env('CI_JOB_NAME', nil)
described_class.configure_rspec
end end
context 'when excluding contexts' do context 'when excluding contexts' do
...@@ -396,7 +387,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do ...@@ -396,7 +387,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with CI_JOB_NAME set' do context 'with CI_JOB_NAME set' do
before do before do
stub_env('CI_JOB_NAME', 'ee:instance-image') stub_env('CI_JOB_NAME', 'ee:instance-image')
described_class.configure_rspec
end end
context 'when excluding contexts' do context 'when excluding contexts' do
......
...@@ -2,9 +2,14 @@ ...@@ -2,9 +2,14 @@
require 'rspec/core/sandbox' require 'rspec/core/sandbox'
RSpec.configure do |c| RSpec.describe QA::Specs::Helpers::Quarantine do
c.around do |ex| include Helpers::StubENV
include QA::Specs::Helpers::RSpec
around do |ex|
RSpec::Core::Sandbox.sandboxed do |config| RSpec::Core::Sandbox.sandboxed do |config|
config.formatter = QA::Specs::Helpers::QuarantineFormatter
# If there is an example-within-an-example, we want to make sure the inner example # If there is an example-within-an-example, we want to make sure the inner example
# does not get a reference to the outer example (the real spec) if it calls # does not get a reference to the outer example (the real spec) if it calls
# something like `pending` # something like `pending`
...@@ -15,18 +20,9 @@ RSpec.configure do |c| ...@@ -15,18 +20,9 @@ RSpec.configure do |c|
ex.run ex.run
end end
end end
end
RSpec.describe QA::Specs::Helpers::Quarantine do
include Helpers::StubENV
include QA::Specs::Helpers::RSpec
describe '.skip_or_run_quarantined_contexts' do describe '.skip_or_run_quarantined_contexts' do
context 'with no tag focused' do context 'with no tag focused' do
before do
described_class.configure_rspec
end
it 'skips before hooks of quarantined contexts' do it 'skips before hooks of quarantined contexts' do
executed_hooks = [] executed_hooks = []
...@@ -66,7 +62,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -66,7 +62,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine focused' do context 'with :quarantine focused' do
before do before do
described_class.configure_rspec
RSpec.configure do |c| RSpec.configure do |c|
c.filter_run :quarantine c.filter_run :quarantine
end end
...@@ -110,10 +105,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -110,10 +105,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
describe '.skip_or_run_quarantined_tests_or_contexts' do describe '.skip_or_run_quarantined_tests_or_contexts' do
context 'with no tag focused' do context 'with no tag focused' do
before do
described_class.configure_rspec
end
it 'skips quarantined tests' do it 'skips quarantined tests' do
group = describe_successfully do group = describe_successfully do
it('is pending', :quarantine) {} it('is pending', :quarantine) {}
...@@ -135,7 +126,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -135,7 +126,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with environment set' do context 'with environment set' do
before do before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
described_class.configure_rspec
end end
context 'no pipeline specified' do context 'no pipeline specified' do
...@@ -168,7 +158,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -168,7 +158,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
shared_examples 'skipped in project' do |project| shared_examples 'skipped in project' do |project|
before do before do
stub_env('CI_PROJECT_NAME', project) stub_env('CI_PROJECT_NAME', project)
described_class.configure_rspec
end end
it "is skipped in #{project}" do it "is skipped in #{project}" do
...@@ -209,7 +198,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -209,7 +198,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine focused' do context 'with :quarantine focused' do
before do before do
described_class.configure_rspec
RSpec.configure do |c| RSpec.configure do |c|
c.filter_run :quarantine c.filter_run :quarantine
end end
...@@ -234,7 +222,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -234,7 +222,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with a non-quarantine tag focused' do context 'with a non-quarantine tag focused' do
before do before do
described_class.configure_rspec
RSpec.configure do |c| RSpec.configure do |c|
c.filter_run :foo c.filter_run :foo
end end
...@@ -277,7 +264,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do ...@@ -277,7 +264,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine and non-quarantine tags focused' do context 'with :quarantine and non-quarantine tags focused' do
before do before do
described_class.configure_rspec
RSpec.configure do |c| RSpec.configure do |c|
c.filter_run :foo, :bar, :quarantine c.filter_run :foo, :bar, :quarantine
end end
......
# frozen_string_literal: true
describe QA::Support::AllureMetadataFormatter do
include Helpers::StubENV
let(:formatter) { described_class.new(StringIO.new) }
let(:rspec_example_notification) { double('RSpec::Core::Notifications::ExampleNotification', example: rspec_example) }
let(:rspec_example) do
double(
'RSpec::Core::Example',
tms: nil,
issue: nil,
add_link: nil,
attempts: 0,
file_path: 'file/path/spec.rb',
metadata: {
testcase: 'testcase',
quarantine: { issue: 'issue' }
}
)
end
let(:ci_job) { 'ee:relative 5' }
let(:ci_job_url) { 'url' }
before do
stub_env('CI', 'true')
stub_env('CI_JOB_NAME', ci_job)
stub_env('CI_JOB_URL', ci_job_url)
end
it "adds additional data to report" do
formatter.example_started(rspec_example_notification)
aggregate_failures do
expect(rspec_example).to have_received(:tms).with('Testcase', 'testcase')
expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue')
expect(rspec_example).to have_received(:add_link).with(name: "Job(#{ci_job})", url: ci_job_url)
expect(rspec_example).to have_received(:issue).with(
'Failure issues',
'https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=spec.rb'
)
end
end
end
...@@ -42,32 +42,6 @@ RSpec.describe ProjectsController do ...@@ -42,32 +42,6 @@ RSpec.describe ProjectsController do
expect(response).not_to render_template('new') expect(response).not_to render_template('new')
end end
end end
context 'when user is an external user' do
let_it_be(:user) { create(:user, external: true) }
it 'responds with status 404' do
group.add_owner(user)
get :new, params: { namespace_id: group.id }
expect(response).to have_gitlab_http_status(:not_found)
expect(response).not_to render_template('new')
end
end
context 'when user is a group guest' do
let_it_be(:user) { create(:user) }
it 'responds with status 404' do
group.add_guest(user)
get :new, params: { namespace_id: group.id }
expect(response).to have_gitlab_http_status(:not_found)
expect(response).not_to render_template('new')
end
end
end end
end end
end end
......
...@@ -585,6 +585,26 @@ RSpec.describe 'Pipelines', :js do ...@@ -585,6 +585,26 @@ RSpec.describe 'Pipelines', :js do
expect(page).to have_selector('.gl-pagination .page-link', count: 4) expect(page).to have_selector('.gl-pagination .page-link', count: 4)
end end
end end
context 'with pipeline key selection' do
before do
visit project_pipelines_path(project)
wait_for_requests
end
it 'changes the Pipeline ID column for Pipeline IID' do
page.find('[data-testid="pipeline-key-dropdown"]').click
within '.gl-new-dropdown-contents' do
dropdown_options = page.find_all '.gl-new-dropdown-item'
dropdown_options[1].click
end
expect(page.find('[data-testid="pipeline-th"]')).to have_content 'Pipeline IID'
expect(page.find('[data-testid="pipeline-url-link"]')).to have_content "##{pipeline.iid}"
end
end
end end
describe 'GET /:project/-/pipelines/show' do describe 'GET /:project/-/pipelines/show' do
......
...@@ -28,6 +28,7 @@ describe('Pipeline Url Component', () => { ...@@ -28,6 +28,7 @@ describe('Pipeline Url Component', () => {
flags: {}, flags: {},
}, },
pipelineScheduleUrl: 'foo', pipelineScheduleUrl: 'foo',
pipelineKey: 'id',
}; };
const createComponent = (props) => { const createComponent = (props) => {
......
...@@ -74,6 +74,7 @@ describe('Pipelines', () => { ...@@ -74,6 +74,7 @@ describe('Pipelines', () => {
const findTablePagination = () => wrapper.findComponent(TablePagination); const findTablePagination = () => wrapper.findComponent(TablePagination);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`); const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
const findPipelineKeyDropdown = () => wrapper.findByTestId('pipeline-key-dropdown');
const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button'); const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button'); const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button'); const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
...@@ -528,6 +529,10 @@ describe('Pipelines', () => { ...@@ -528,6 +529,10 @@ describe('Pipelines', () => {
expect(findFilteredSearch().exists()).toBe(true); expect(findFilteredSearch().exists()).toBe(true);
}); });
it('renders the pipeline key dropdown', () => {
expect(findPipelineKeyDropdown().exists()).toBe(true);
});
it('renders tab empty state finished scope', async () => { it('renders tab empty state finished scope', async () => {
mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, { mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
pipelines: [], pipelines: [],
...@@ -623,6 +628,10 @@ describe('Pipelines', () => { ...@@ -623,6 +628,10 @@ describe('Pipelines', () => {
expect(findFilteredSearch().exists()).toBe(false); expect(findFilteredSearch().exists()).toBe(false);
}); });
it('does not render the pipeline key dropdown', () => {
expect(findPipelineKeyDropdown().exists()).toBe(false);
});
it('does not render tabs nor buttons', () => { it('does not render tabs nor buttons', () => {
expect(findNavigationTabs().exists()).toBe(false); expect(findNavigationTabs().exists()).toBe(false);
expect(findTab('all').exists()).toBe(false); expect(findTab('all').exists()).toBe(false);
......
...@@ -8,6 +8,7 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr ...@@ -8,6 +8,7 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue'; import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue'; import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import { PipelineKeyOptions } from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub'; import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
...@@ -24,6 +25,7 @@ describe('Pipelines Table', () => { ...@@ -24,6 +25,7 @@ describe('Pipelines Table', () => {
const defaultProps = { const defaultProps = {
pipelines: [], pipelines: [],
viewType: 'root', viewType: 'root',
pipelineKeyOption: PipelineKeyOptions[0],
}; };
const createMockPipeline = () => { const createMockPipeline = () => {
...@@ -80,7 +82,7 @@ describe('Pipelines Table', () => { ...@@ -80,7 +82,7 @@ describe('Pipelines Table', () => {
it('should render table head with correct columns', () => { it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status'); expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline'); expect(findPipelineTh().text()).toBe('Pipeline ID');
expect(findTriggererTh().text()).toBe('Triggerer'); expect(findTriggererTh().text()).toBe('Triggerer');
expect(findCommitTh().text()).toBe('Commit'); expect(findCommitTh().text()).toBe('Commit');
expect(findStagesTh().text()).toBe('Stages'); expect(findStagesTh().text()).toBe('Stages');
......
...@@ -144,23 +144,6 @@ describe('RelatedIssuableItem', () => { ...@@ -144,23 +144,6 @@ describe('RelatedIssuableItem', () => {
expect(wrapper.find(IssueDueDate).props('closed')).toBe(true); expect(wrapper.find(IssueDueDate).props('closed')).toBe(true);
}); });
it('should not contain the `.text-danger` css class for overdue issue that is closed', async () => {
mountComponent({
props: {
...props,
closedAt: '2018-12-01T00:00:00.00Z',
},
});
await wrapper.vm.$nextTick();
expect(wrapper.find(IssueDueDate).find('.board-card-info-icon').classes('text-danger')).toBe(
false,
);
expect(wrapper.find(IssueDueDate).find('.board-card-info-text').classes('text-danger')).toBe(
false,
);
});
}); });
describe('token assignees', () => { describe('token assignees', () => {
......
...@@ -13,12 +13,13 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence ...@@ -13,12 +13,13 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
let(:vulnerabilities) { table(:vulnerabilities) } let(:vulnerabilities) { table(:vulnerabilities) }
let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) } let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let(:vulnerability_identifier) do let(:vulnerability_identifier) do
vulnerability_identifiers.create!( vulnerability_identifiers.create!(
project_id: project.id, project_id: project.id,
external_type: 'uuid-v5', external_type: 'uuid-v5',
external_id: 'uuid-v5', external_id: 'uuid-v5',
fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', fingerprint: Gitlab::Database::ShaAttribute.serialize('7e394d1b1eb461a7406d7b1e08f057a1cf11287a'),
name: 'Identifier for UUIDv5') name: 'Identifier for UUIDv5')
end end
...@@ -27,7 +28,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence ...@@ -27,7 +28,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
project_id: project.id, project_id: project.id,
external_type: 'uuid-v4', external_type: 'uuid-v4',
external_id: 'uuid-v4', external_id: 'uuid-v4',
fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89', fingerprint: Gitlab::Database::ShaAttribute.serialize('772da93d34a1ba010bcb5efa9fb6f8e01bafcc89'),
name: 'Identifier for UUIDv4') name: 'Identifier for UUIDv4')
end end
...@@ -59,7 +60,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence ...@@ -59,7 +60,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: different_scanner.id, scanner_id: different_scanner.id,
primary_identifier_id: different_vulnerability_identifier.id, primary_identifier_id: different_vulnerability_identifier.id,
report_type: 0, # "sast" report_type: 0, # "sast"
location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e", location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4 uuid: known_uuid_v4
) )
end end
...@@ -91,7 +92,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence ...@@ -91,7 +92,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: scanner.id, scanner_id: scanner.id,
primary_identifier_id: vulnerability_identifier.id, primary_identifier_id: vulnerability_identifier.id,
report_type: 0, # "sast" report_type: 0, # "sast"
location_fingerprint: "838574be0210968bf6b9f569df9c2576242cbf0a", location_fingerprint: Gitlab::Database::ShaAttribute.serialize("838574be0210968bf6b9f569df9c2576242cbf0a"),
uuid: known_uuid_v5 uuid: known_uuid_v5
) )
end end
...@@ -115,7 +116,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence ...@@ -115,7 +116,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: different_scanner.id, scanner_id: different_scanner.id,
primary_identifier_id: different_vulnerability_identifier.id, primary_identifier_id: different_vulnerability_identifier.id,
report_type: 0, # "sast" report_type: 0, # "sast"
location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e", location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4 uuid: known_uuid_v4
) )
......
...@@ -87,12 +87,18 @@ RSpec.describe Gitlab::EtagCaching::Router::Restful do ...@@ -87,12 +87,18 @@ RSpec.describe Gitlab::EtagCaching::Router::Restful do
end end
it 'matches the environments path' do it 'matches the environments path' do
result = match_route('/my-group/my-project/environments.json') result = match_route('/my-group/my-project/-/environments.json')
expect(result).to be_present expect(result).to be_present
expect(result.name).to eq 'environments' expect(result.name).to eq 'environments'
end end
it 'does not match the operations environments list path' do
result = match_route('/-/operations/environments.json')
expect(result).not_to be_present
end
it 'matches pipeline#show endpoint' do it 'matches pipeline#show endpoint' do
result = match_route('/my-group/my-project/-/pipelines/2.json') result = match_route('/my-group/my-project/-/pipelines/2.json')
......
...@@ -118,6 +118,24 @@ RSpec.describe Gitlab::UsageDataMetrics do ...@@ -118,6 +118,24 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject[:redis_hll_counters][:quickactions].keys).to include(*known_events_keys) expect(subject[:redis_hll_counters][:quickactions].keys).to include(*known_events_keys)
end end
it 'includes terraform monthly key' do
expect(subject[:redis_hll_counters][:terraform].keys).to include(:p_terraform_state_api_unique_users_monthly)
end
it 'includes terraform monthly and weekly keys' do
expect(subject[:redis_hll_counters][:pipeline_authoring].keys).to contain_exactly(*[
:o_pipeline_authoring_unique_users_committing_ciconfigfile_monthly, :o_pipeline_authoring_unique_users_committing_ciconfigfile_weekly,
:o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile_monthly, :o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile_weekly,
:pipeline_authoring_total_unique_counts_monthly, :pipeline_authoring_total_unique_counts_weekly
])
end
it 'includes users_expanding_secure_security_report monthly and weekly keys' do
expect(subject[:redis_hll_counters][:secure].keys).to contain_exactly(*[
:users_expanding_secure_security_report_monthly, :users_expanding_secure_security_report_weekly
])
end
it 'includes issues_edit monthly and weekly keys' do it 'includes issues_edit monthly and weekly keys' do
expect(subject[:redis_hll_counters][:issues_edit].keys).to include( expect(subject[:redis_hll_counters][:issues_edit].keys).to include(
:g_project_management_issue_title_changed_monthly, :g_project_management_issue_title_changed_weekly, :g_project_management_issue_title_changed_monthly, :g_project_management_issue_title_changed_weekly,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment