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 @@
"Geo",
"Git LFS",
"git-annex",
"git-sizer",
"Git",
"Gitaly",
"GitHub",
......
0a4e1c785063f4ad2cef80303fa10276e2e4e2a6
b3d56404cc25983d1bffd015fe0d29c1d50eab58
......@@ -2,6 +2,7 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { getParameterByName } from '~/lib/utils/url_utility';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { PipelineKeyOptions } from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
import PipelinesService from '~/pipelines/services/pipelines_service';
......@@ -10,6 +11,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
PipelineKeyOptions,
components: {
GlButton,
GlEmptyState,
......@@ -205,6 +207,7 @@ export default {
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
:pipeline-key-option="$options.PipelineKeyOptions[0]"
>
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
......
......@@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
pipelineKey: {
type: String,
required: true,
},
},
computed: {
user() {
......@@ -60,7 +64,7 @@ export default {
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
#{{ pipeline.id }}
#{{ pipeline[pipelineKey] }}
</gl-link>
<div class="label-container">
<gl-badge
......
<script>
import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
import createFlash from '~/flash';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.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 PipelinesService from '../../services/pipelines_service';
import { validateParams } from '../../utils';
......@@ -16,8 +21,11 @@ import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import PipelinesTableComponent from './pipelines_table.vue';
export default {
PipelineKeyOptions,
components: {
EmptyState,
GlDropdown,
GlDropdownItem,
GlEmptyState,
GlIcon,
GlLoadingIcon,
......@@ -114,6 +122,7 @@ export default {
page: getParameterByName('page') || '1',
requestData: {},
isResetCacheButtonLoading: false,
selectedPipelineKeyOption: this.$options.PipelineKeyOptions[0],
};
},
stateMap: {
......@@ -301,6 +310,9 @@ export default {
this.updateContent(this.requestData);
},
changeVisibilityPipelineID(val) {
this.selectedPipelineKeyOption = val;
},
},
};
</script>
......@@ -330,12 +342,31 @@ export default {
/>
</div>
<div v-if="stateToRender !== $options.stateMap.emptyState" class="gl-display-flex">
<div class="row-content-block gl-display-flex gl-flex-grow-1">
<pipelines-filtered-search
v-if="stateToRender !== $options.stateMap.emptyState"
class="gl-display-flex gl-flex-grow-1 gl-mr-4"
: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">
<gl-loading-icon
......@@ -374,6 +405,7 @@ export default {
:pipeline-schedule-url="pipelineScheduleUrl"
:update-graph-dropdown="updateGraphDropdown"
:view-type="viewType"
:pipeline-key-option="selectedPipelineKeyOption"
/>
</div>
......
......@@ -101,12 +101,10 @@ export default {
</script>
<template>
<div class="row-content-block">
<gl-filtered-search
v-model="value"
:placeholder="__('Filter pipelines')"
:available-tokens="tokens"
@submit="onSubmit"
/>
</div>
</template>
......@@ -17,7 +17,57 @@ 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!';
export default {
fields: [
components: {
GlTable,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
PipelineMiniGraph,
PipelineOperations,
PipelinesStatusBadge,
PipelineStopModal,
PipelinesTimeago,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
pipelines: {
type: Array,
required: true,
},
pipelineScheduleUrl: {
type: String,
required: false,
default: '',
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
viewType: {
type: String,
required: true,
},
pipelineKeyOption: {
type: Object,
required: true,
},
},
data() {
return {
pipelineId: 0,
pipeline: {},
endpoint: '',
cancelingPipeline: null,
};
},
computed: {
tableFields() {
const fields = [
{
key: 'status',
label: s__('Pipeline|Status'),
......@@ -28,7 +78,7 @@ export default {
},
{
key: 'pipeline',
label: s__('Pipeline|Pipeline'),
label: this.pipelineKeyOption.label,
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
......@@ -73,51 +123,10 @@ export default {
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'actions-th' },
},
],
components: {
GlTable,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
PipelinesCommit,
PipelineMiniGraph,
PipelineOperations,
PipelinesStatusBadge,
PipelineStopModal,
PipelinesTimeago,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
pipelines: {
type: Array,
required: true,
},
pipelineScheduleUrl: {
type: String,
required: false,
default: '',
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
viewType: {
type: String,
required: true,
];
return fields;
},
},
data() {
return {
pipelineId: 0,
pipeline: {},
endpoint: '',
cancelingPipeline: null,
};
},
watch: {
pipelines() {
this.cancelingPipeline = null;
......@@ -148,7 +157,7 @@ export default {
<template>
<div class="ci-table">
<gl-table
:fields="$options.fields"
:fields="tableFields"
:items="pipelines"
tbody-tr-class="commit"
:tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }"
......@@ -169,7 +178,11 @@ export default {
</template>
<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 #cell(triggerer)="{ item }">
......
......@@ -35,3 +35,17 @@ export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
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
# rubocop: disable CodeReuse/ActiveRecord
def new
return access_denied! unless current_user.can_create_project?
@namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace)
......
......@@ -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: 'starred', title: _('Starred projects'), href: starred_dashboard_projects_path)
builder.add_primary_menu_item(id: 'explore', title: _('Explore projects'), href: explore_root_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
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
scope :with_project_and_metadata, -> do
if Feature.enabled?(:non_public_artifacts, type: :development)
joins(:metadata).includes(:project, :metadata)
joins(:metadata).includes(:metadata).preload(:project)
end
end
......
......@@ -10,6 +10,7 @@ module Analytics
included do
belongs_to :start_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, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
......@@ -28,6 +29,9 @@ module Analytics
scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
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
def parent=(_)
......@@ -133,6 +137,20 @@ module Analytics
.id_in(label_id)
.exists?
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
......@@ -14,9 +14,10 @@ class WebHookWorker
worker_has_external_dependencies!
def perform(hook_id, data, hook_name)
hook = WebHook.find(hook_id)
data = data.with_indifferent_access
hook = WebHook.find_by_id(hook_id)
return unless hook
data = data.with_indifferent_access
WebHookService.new(hook, data, hook_name, jid).execute
end
end
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- p_terraform_state_api_unique_users
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_committing_ciconfigfile
distribution:
- ee
- ce
......
......@@ -12,6 +12,10 @@ milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54707
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile
distribution:
- ce
- ee
......
......@@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_secure_security_report
distribution:
- ce
- ee
......
......@@ -12,7 +12,12 @@ status: data_available
milestone: "13.12"
introduced_by_url:
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:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_committing_ciconfigfile
distribution:
- ee
- ce
......
......@@ -12,6 +12,10 @@ milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54707
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile
distribution:
- ce
- ee
......
......@@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_secure_security_report
distribution:
- ce
- ee
......
......@@ -12,7 +12,12 @@ status: data_available
milestone: "13.12"
introduced_by_url:
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:
- ce
- 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 (
hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true 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
......@@ -9128,7 +9129,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages (
hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true 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
......@@ -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;
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 (
id bigint NOT NULL,
last_recorded_at timestamp with time zone,
......@@ -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_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_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
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
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
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
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 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
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 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
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_packages_size_and_project_id ON project_statistics USING btree (packages_size, project_id);
......@@ -26073,6 +26100,9 @@ ALTER TABLE ONLY members
ALTER TABLE ONLY lfs_objects_projects
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
ADD CONSTRAINT fk_30f2a831f4 FOREIGN KEY (iteration_id) REFERENCES sprints(id) ON DELETE CASCADE;
......@@ -26514,6 +26544,9 @@ ALTER TABLE ONLY packages_packages
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;
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
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:
- 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)
- 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).
......
......@@ -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
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.
- 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
......@@ -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:
```yaml
stages:
- chatops
hello-world:
stage: chatops
only: [chat]
rules:
- if: '$CI_PIPELINE_SOURCE == "chat"'
script:
- echo "Hello World"
```
......@@ -81,9 +85,13 @@ the `chat_reply` section. For example, the following job lists the files in the
current directory:
```yaml
stages:
- chatops
ls:
stage: chatops
only: [chat]
rules:
- if: '$CI_PIPELINE_SOURCE == "chat"'
script:
- 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"
......
......@@ -14,15 +14,16 @@ GitLab CI/CD can be used with Bitbucket Cloud by:
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
**Repo by URL** and create the project.
<!-- vale gitlab.Spelling = YES -->
1. In GitLab, create a project:
1. On the top menu, select **Projects > Create new project**.
1. Select **Run CI/CD for external repository**.
1. Select **Repo by URL**.
![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).
1. In GitLab create a
1. In GitLab, create a
[Personal Access Token](../../user/profile/personal_access_tokens.md)
with `api` scope. This is used to authenticate requests from the web
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:
\"$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.
```yaml
......
......@@ -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
repositories:
1. Open <https://github.com/settings/tokens/new> to create a **Personal Access
Token**. This token is used to access your repository and push commit
statuses to GitHub.
The `repo` and `admin:repo_hook` should be enable to allow GitLab access to
your project, update commit statuses, and create a web hook to notify
GitLab of new commits.
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
**GitHub**.
1. Paste the token into the **Personal access token** field and click **List
Repositories**. Click **Connect** to select the repository.
1. In GitHub, create a token:
1. Open <https://github.com/settings/tokens/new>.
1. Create a **Personal Access Token**.
1. Enter a **Token description** and update the scope to allow
`repo` and `admin:repo_hook` so that GitLab can access your project,
update commit statuses, and create a web hook to notify GitLab of new commits.
1. In GitLab, create a project:
1. On the top menu, select **Projects > Create new project**.
1. Select **Run CI/CD for external repository**.
1. Select **GitHub**.
1. For **Personal access token**, paste the token.
1. Select **List Repositories**.
1. Select **Connect** to select the repository.
1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/index.md).
GitLab:
1. Imports the project.
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 [Pull Mirroring](../../user/project/repository/repository_mirroring.md#pull-from-a-remote-repository).
1. Enables [GitHub project integration](../../user/project/integrations/github.md).
1. Creates a web hook on GitHub to notify GitLab of new commits.
## Connect manually
......@@ -56,30 +55,25 @@ To use **GitHub Enterprise** with **GitLab.com**, use this method.
To manually enable GitLab CI/CD for your repository:
1. In GitHub open <https://github.com/settings/tokens/new> create a **Personal
Access Token.** GitLab uses this token to access your repository and
push commit statuses.
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 **CI/CD project** using the Git URL option and the HTTPS
URL for your GitHub repository. If your project is private, use the personal
access token you just created for authentication.
1. In GitHub, create a token:
1. Open <https://github.com/settings/tokens/new>.
1. Create a **Personal Access Token**.
1. 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:
1. On the top menu, select **Projects > Create new project**.
1. Select **Run CI/CD for external repository** and **Repo by URL**.
1. In the **Git repository URL** field, enter the HTTPS URL for your GitHub repository.
If your project is private, use the personal 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.
1. Still in GitLab, enable the [GitHub project integration](../../user/project/integrations/github.md)
from **Settings > Integrations.**
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
1. In GitLab, enable [GitHub project integration](../../user/project/integrations/github.md):
1. On the left sidebar, select **Settings > Integrations**.
1. Select the **Active** checkbox.
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
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.
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:
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
......
......@@ -27,10 +27,10 @@ To connect to an external repository:
<!-- vale gitlab.Spelling = NO -->
1. From your GitLab dashboard, click **New project**.
1. Switch to the **CI/CD for external repository** tab.
1. Choose **GitHub** or **Repo by URL**.
1. The next steps are similar to the [import flow](../../user/project/import/index.md).
1. On the top menu, select **Projects > Create new project**.
1. Select **Run CI/CD for external repository**.
1. Select **GitHub** or **Repo by URL**.
1. Complete the fields.
<!-- vale gitlab.Spelling = YES -->
......
......@@ -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))
- 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
Pipelines can be manually executed, with predefined or manually-specified [variables](../variables/index.md).
......@@ -167,6 +170,9 @@ variables:
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
> [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 `.
- `http://example.com/generate/ci/config.yml`
If the CI/CD configuration file is in a different project, the path must be relative
to the root directory in the other project. Include the group and project name at the end:
If the CI/CD configuration file is in a different project:
- 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`
- `my/path/.my-custom-file.yml@mygroup/another-project`
......
......@@ -1530,13 +1530,16 @@ production:
#### Requirements and limitations
- In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632)
you can refer to jobs in the same stage as the job you are configuring. This feature
is [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
- Disabled on GitLab.com.
- Not recommended for production use.
- For GitLab self-managed instances, GitLab adminsitrators
can choose to [disable it](#enable-or-disable-needs-for-jobs-in-the-same-stage)
- In [GitLab 14.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/30632) you
can refer to jobs in the same stage as the job you are configuring. This feature is:
- [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
- Disabled on GitLab.com.
- Not recommended for production use.
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 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.
......
......@@ -72,12 +72,17 @@ issue should have one and only one.
The current type labels are:
- ~feature
- ~bug
- ~tooling
- ~"support request"
- ~meta
- ~documentation
- `~feature`
- `~"feature::addition"`
- `~"feature::enhancement"`
- `~"feature::maintenance"`
- `~bug`
- `~tooling`
- `~"tooling::pipelines"`
- `~"tooling::workflow"`
- `~"support request"`
- `~meta`
- `~documentation`
A number of type labels have a priority assigned to them, which automatically
makes them float to the top, depending on their importance.
......
......@@ -113,7 +113,7 @@ patterns may apply to future cases.
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
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:
- <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
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.
#### 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-normalization refers to adding redundant precomputed (duplicated) data to
......@@ -243,3 +257,37 @@ A quick checklist for fixing a specific join query would be:
adding a new column
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
#### 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
the commits in your terminal's text editor.
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):
1. Press <!-- vale gitlab.FirstPerson = NO --> <kbd>i</kbd> <!-- vale gitlab.FirstPerson = YES -->
on your keyboard to switch to Vim's editing mode.
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 second and third into the first.
1. Press <kbd>Escape</kbd> to leave the editing mode.
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
message. Everything else is included.
- 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
- They take up a large amount of storage space on the server.
- 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.
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
......
......@@ -210,3 +210,59 @@ To help avoid abuse, by default, users are rate limited to:
| Import | 6 projects per minute |
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 {
<item-due-date
v-if="item.dueDate"
:date="item.dueDate"
:closed="Boolean(item.closedAt)"
tooltip-placement="top"
css-class="item-due-date gl-display-flex gl-align-items-center gl-mr-5!"
/>
......
<script>
import { mapActions } from 'vuex';
import NoEnvironmentEmptyState from '../no_environment_empty_state.vue';
import PoliciesHeader from './policies_header.vue';
import PoliciesList from './policies_list.vue';
export default {
components: {
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>
<template>
<div>
<policies-header />
<no-environment-empty-state v-if="!shouldFetchEnvironment" />
<policies-list v-else />
</div>
</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 () => {
} = el.dataset;
const store = createStore();
store.dispatch('threatMonitoring/setEndpoints', {
networkPolicyStatisticsEndpoint,
environmentsEndpoint,
});
store.dispatch('threatMonitoring/setStatisticsEndpoint', networkPolicyStatisticsEndpoint);
store.dispatch('threatMonitoring/setEnvironmentEndpoint', environmentsEndpoint);
return new Vue({
apolloProvider,
......
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import SecurityPoliciesApp from './components/policies/policies_app.vue';
import createStore from './store';
Vue.use(VueApollo);
......@@ -15,13 +16,20 @@ export default () => {
const {
assignedPolicyProject,
disableSecurityPolicyProject,
defaultEnvironmentId,
environmentsEndpoint,
emptyStateSvgPath,
documentationPath,
newPolicyPath,
projectPath,
} = el.dataset;
const store = createStore();
store.dispatch('threatMonitoring/setEnvironmentEndpoint', environmentsEndpoint);
return new Vue({
apolloProvider,
store,
el,
provide: {
assignedPolicyProject: JSON.parse(assignedPolicyProject),
......@@ -29,6 +37,8 @@ export default () => {
documentationPath,
newPolicyPath,
projectPath,
emptyStateSvgPath,
defaultEnvironmentId: parseInt(defaultEnvironmentId, 10),
},
render(createElement) {
return createElement(SecurityPoliciesApp);
......
......@@ -3,13 +3,12 @@ import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINT, endpoints.environmentsEndpoint);
commit(
`threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
endpoints.networkPolicyStatisticsEndpoint,
{ root: true },
);
export const setEnvironmentEndpoint = ({ commit }, endpoint) => {
commit(types.SET_ENDPOINT, endpoint);
};
export const setStatisticsEndpoint = ({ commit }, endpoint) => {
commit(`threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`, endpoint, { root: true });
};
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")
- 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,
default_environment_id: default_environment_id,
disable_security_policy_project: disable_security_policy_project.to_s,
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),
environments_endpoint: project_environments_path(project),
project_path: project.full_path } }
......@@ -9,10 +9,10 @@
%p.gl-text-center
- if remove_known_trial_form_fields_variant == :welcoming
- salutation = current_user.first_name.present? ? " #{current_user.first_name}" : ''
- company = current_user.organization.present? ? current_user.organization : _('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 }
- company = current_user.organization.present? ? current_user.organization : s_('Trial|your 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
= _('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'
......@@ -21,36 +21,36 @@
= hidden_field_tag :first_name, current_user.first_name
- else
.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?
= 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?
= hidden_field_tag :last_name, current_user.last_name
- else
.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?
= 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?
= hidden_field_tag :company_name, current_user.organization
- else
.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?
= 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
= 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' }
.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' }
%p.gl-text-gray-500= _('Allowed characters: +, 0-9, -, and spaces.')
.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' }
.form-group.gl-select2-html5-required-fix
= label_tag :country, _('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' }
= submit_tag _('Continue'), class: 'btn gl-button btn-confirm btn-block', data: { qa_selector: 'continue' }
= label_tag :country, s_('Trial|Country'), class: 'col-form-label'
= 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 s_('Trial|Continue'), class: 'btn gl-button btn-confirm btn-block', data: { qa_selector: 'continue' }
= render 'skip_trial'
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_ci_secrets_management_vault_build_created
distribution:
- ee
tier:
......
......@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_creating_epic_boards
distribution:
- ee
tier:
......
......@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_viewing_epic_boards
distribution:
- ee
tier:
......
......@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_updating_epic_board_names
distribution:
- ee
tier:
......
......@@ -12,7 +12,13 @@ status: data_available
milestone: "13.12"
introduced_by_url:
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:
- ee
tier:
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_ci_secrets_management_vault_build_created
distribution:
- ee
tier: []
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- p_terraform_state_api_unique_users
distribution:
- ee
tier: []
......
......@@ -15,6 +15,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_creating_epic_boards
distribution:
- ee
tier:
......
......@@ -14,6 +14,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_viewing_epic_boards
distribution:
- ee
tier:
......
......@@ -14,6 +14,10 @@ milestone: "13.12"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- g_project_management_users_updating_epic_board_names
distribution:
- ee
tier:
......
......@@ -12,7 +12,13 @@ status: data_available
milestone: "13.12"
introduced_by_url:
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:
- ee
tier:
......
......@@ -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_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_compliance_framework_changes
......
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { PipelineKeyOptions } from '~/pipelines/constants';
import { triggeredBy, triggered } from './mock_data';
......@@ -15,6 +16,7 @@ describe('Pipelines Table', () => {
const defaultProps = {
pipelines: [],
viewType: 'root',
pipelineKeyOption: PipelineKeyOptions[0],
};
const createMockPipeline = () => {
......
......@@ -361,6 +361,19 @@ describe('RelatedItemsTree', () => {
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', () => {
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 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';
describe('Policies App', () => {
let wrapper;
let store;
let setCurrentEnvironmentIdSpy;
let fetchEnvironmentsSpy;
const findPoliciesHeader = () => wrapper.findComponent(PoliciesHeader);
const findPoliciesList = () => wrapper.findComponent(PoliciesList);
const findEmptyState = () => wrapper.findComponent(NoEnvironmentEmptyState);
beforeEach(() => {
wrapper = shallowMountExtended(PoliciesApp);
const createWrapper = ({ provide } = {}) => {
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(() => {
wrapper.destroy();
});
describe('when does have an environment enabled', () => {
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 * 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 getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state';
......@@ -26,24 +25,33 @@ describe('Threat Monitoring actions', () => {
createFlash.mockClear();
});
describe('setEndpoints', () => {
describe('threatMonitoring/setStatisticsEndpoint', () => {
it('commits the SET_ENDPOINT mutation', () =>
testAction(
actions.setEndpoints,
{
environmentsEndpoint,
actions.setStatisticsEndpoint,
networkPolicyStatisticsEndpoint,
state,
[
{
type: `threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
payload: networkPolicyStatisticsEndpoint,
},
],
[],
));
});
describe('threatMonitoring/setEnvironmentEndpoint', () => {
it('commits the SET_ENDPOINT mutation', () =>
testAction(
actions.setEnvironmentEndpoint,
environmentsEndpoint,
state,
[
{
type: types.SET_ENDPOINT,
payload: environmentsEndpoint,
},
{
type: `threatMonitoringNetworkPolicy/${types.SET_ENDPOINT}`,
payload: networkPolicyStatisticsEndpoint,
},
],
[],
));
......
......@@ -14,7 +14,10 @@ RSpec.describe EE::Audit::ProjectChangesAuditor do
repository_size_limit: 10,
packages_enabled: true,
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
......@@ -162,6 +165,45 @@ RSpec.describe EE::Audit::ProjectChangesAuditor do
)
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
......@@ -54,6 +54,25 @@ RSpec.describe Gitlab::UsageDataMetrics do
])
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
expect(subject[:redis_hll_counters][:issues_edit].keys).to include(
: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
end
class VulnerabilitiesFinding < ActiveRecord::Base
include ShaAttribute
self.table_name = "vulnerability_occurrences"
belongs_to :primary_identifier, class_name: 'VulnerabilitiesIdentifier', inverse_of: :primary_findings, foreign_key: 'primary_identifier_id'
REPORT_TYPES = {
......@@ -21,6 +23,9 @@ class Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid
api_fuzzing: 6
}.with_indifferent_access.freeze
enum report_type: REPORT_TYPES
sha_attribute :fingerprint
sha_attribute :location_fingerprint
end
class CalculateFindingUUID
......
......@@ -71,7 +71,7 @@ module Gitlab
'continuous_delivery'
],
[
%r(#{RESERVED_WORDS_PREFIX}/environments\.json\z),
%r(#{RESERVED_WORDS_PREFIX}/-/environments\.json\z),
'environments',
'continuous_delivery'
],
......
......@@ -14,7 +14,7 @@ module Gitlab
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
].freeze
HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [
SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
EOFError, SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep
].freeze
......
......@@ -8209,9 +8209,6 @@ msgstr ""
msgid "Company"
msgstr ""
msgid "Company name"
msgstr ""
msgid "Compare"
msgstr ""
......@@ -9289,9 +9286,6 @@ msgstr ""
msgid "Couldn't assign policy to project"
msgstr ""
msgid "Country"
msgstr ""
msgid "Coverage"
msgstr ""
......@@ -16370,9 +16364,6 @@ msgstr ""
msgid "Hi %{username}!"
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"
msgstr ""
......@@ -16509,9 +16500,6 @@ msgstr ""
msgid "How many seconds an IP will be counted towards the limit"
msgstr ""
msgid "How many users will be evaluating the trial?"
msgstr ""
msgid "I accept the %{terms_link}"
msgstr ""
......@@ -22918,9 +22906,6 @@ msgstr ""
msgid "Number of commits per MR"
msgstr ""
msgid "Number of employees"
msgstr ""
msgid "Number of events"
msgstr ""
......@@ -24189,6 +24174,12 @@ msgstr ""
msgid "Pipeline %{label} for \"%{dataTitle}\""
msgstr ""
msgid "Pipeline ID"
msgstr ""
msgid "Pipeline IID"
msgstr ""
msgid "Pipeline Schedule"
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."
msgstr ""
msgid "Show Pipeline ID"
msgstr ""
msgid "Show Pipeline IID"
msgstr ""
msgid "Show all activity"
msgstr ""
......@@ -32449,9 +32446,6 @@ msgstr ""
msgid "TeamcityIntegration|Trigger TeamCity CI after every push to the repository, except branch delete"
msgstr ""
msgid "Telephone number"
msgstr ""
msgid "Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}."
msgstr ""
......@@ -35011,6 +35005,9 @@ msgstr ""
msgid "Trial|Company name"
msgstr ""
msgid "Trial|Continue"
msgstr ""
msgid "Trial|Continue using the basic features of GitLab for free."
msgstr ""
......@@ -35020,15 +35017,30 @@ msgstr ""
msgid "Trial|Dismiss"
msgstr ""
msgid "Trial|First name"
msgstr ""
msgid "Trial|GitLab Ultimate trial (optional)"
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?"
msgstr ""
msgid "Trial|How many users will be evaluating the trial?"
msgstr ""
msgid "Trial|Last name"
msgstr ""
msgid "Trial|Number of employees"
msgstr ""
msgid "Trial|Please select a country"
msgstr ""
msgid "Trial|Successful trial activation image"
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:"
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"
msgstr ""
......@@ -38233,9 +38251,6 @@ msgstr ""
msgid "Your GPG keys (%{count})"
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."
msgstr ""
......@@ -40180,8 +40195,5 @@ msgstr ""
msgid "yaml invalid"
msgstr ""
msgid "your company"
msgstr ""
msgid "your settings"
msgstr ""
......@@ -632,7 +632,9 @@ module QA
module Helpers
autoload :ContextSelector, 'qa/specs/helpers/context_selector'
autoload :ContextFormatter, 'qa/specs/helpers/context_formatter'
autoload :Quarantine, 'qa/specs/helpers/quarantine'
autoload :QuarantineFormatter, 'qa/specs/helpers/quarantine_formatter'
autoload :RSpec, 'qa/specs/helpers/rspec'
end
end
......@@ -680,6 +682,7 @@ module QA
autoload :WaitForRequests, 'qa/support/wait_for_requests'
autoload :OTP, 'qa/support/otp'
autoload :SSH, 'qa/support/ssh'
autoload :AllureMetadataFormatter, 'qa/support/allure_metadata_formatter.rb'
end
end
......
......@@ -67,25 +67,8 @@ module QA
# @return [void]
def configure_rspec
RSpec.configure do |config|
config.formatter = AllureRspecFormatter
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
config.add_formatter(AllureRspecFormatter)
config.add_formatter(QA::Support::AllureMetadataFormatter)
end
end
......
......@@ -96,9 +96,6 @@ module QA
end
after do |example|
# skip saving data if example is skipped or failed before import finished
next if example.pending?
user.remove_via_api!
next unless defined?(@import_time)
......
# frozen_string_literal: true
module QA
# This test was quarantined because relative URL isn't supported
# See https://gitlab.com/gitlab-org/gitlab/issues/13833
RSpec.describe 'Create', :runner, :quarantine do
RSpec.describe(
'Create',
:runner,
quarantine: {
only: { job: 'relative_url' },
issue: 'https://gitlab.com/gitlab-org/gitlab/issues/13833',
type: :bug
}
) do
describe 'Web IDE web terminal' do
before do
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
module ContextSelector
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)
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? }
......
......@@ -10,26 +10,6 @@ module QA
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.
def skip_or_run_quarantined_tests_or_contexts(filters, example)
if filters.key?(:quarantine)
......@@ -43,19 +23,19 @@ module QA
# 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
# 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)
else
if example.metadata.key?(:quarantine)
if should_skip_when_focused?(example.metadata, included_filters)
example.metadata[:skip] = "Only running tests tagged with :quarantine and any of #{included_filters.keys}"
end
elsif example.metadata.key?(:quarantine)
quarantine_tag = example.metadata[:quarantine]
if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only)
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 unless ContextSelector.context_matches?(quarantine_tag[:only])
return
end
skip(quarantine_message(quarantine_tag))
end
example.metadata[:skip] = quarantine_message(quarantine_tag)
end
end
......@@ -64,7 +44,7 @@ module QA
end
def quarantine_message(quarantine_tag)
quarantine_message = %w(In quarantine)
quarantine_message = %w[In quarantine]
quarantine_message << case quarantine_tag
when String
": #{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
# expanding into the global state
# See: https://github.com/rspec/rspec-core/issues/2603
def describe_successfully(*args, &describe_body)
reporter = ::RSpec.configuration.reporter
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
example_group
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
RSpec.configure do |config|
config.include ::Matchers
QA::Specs::Helpers::Quarantine.configure_rspec
QA::Specs::Helpers::ContextSelector.configure_rspec
config.add_formatter QA::Specs::Helpers::ContextFormatter
config.add_formatter QA::Specs::Helpers::QuarantineFormatter
config.before do |example|
QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n")
......
......@@ -5,21 +5,7 @@ require 'allure-rspec'
describe QA::Runtime::AllureReport do
include Helpers::StubENV
let(:rspec_config) { double('RSpec::Core::Configuration', '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(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, after: nil) }
let(:png_path) { 'png_path' }
let(:html_path) { 'html_path' }
......@@ -36,7 +22,6 @@ describe QA::Runtime::AllureReport do
allow(AllureRspec).to receive(:configure).and_yield(allure_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_html).and_yield(html_path)
end
......@@ -62,12 +47,10 @@ describe QA::Runtime::AllureReport do
let(:png_file) { 'png-file' }
let(:html_file) { 'html-file' }
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)
allow(Allure).to receive(:add_attachment)
allow(File).to receive(:open).with(png_path) { png_file }
......@@ -85,20 +68,9 @@ describe QA::Runtime::AllureReport do
end
end
it 'adds rspec formatter' do
expect(rspec_config).to have_received(:formatter=).with(AllureRspecFormatter)
end
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
it 'adds rspec and metadata formatter' do
expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered
expect(rspec_config).to have_received(:add_formatter).with(QA::Support::AllureMetadataFormatter).ordered
end
it 'configures screenshot saving' do
......
......@@ -2,29 +2,25 @@
require 'rspec/core/sandbox'
RSpec.configure do |c|
c.around do |ex|
RSpec.describe QA::Specs::Helpers::ContextSelector do
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|
config.formatter = QA::Specs::Helpers::ContextFormatter
# 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
# something like `pending`
config.before(:context) { RSpec.current_example = nil }
config.color_mode = :off
ex.run
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
it 'returns true when url has .com' do
......@@ -104,7 +100,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with different environment set' do
before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com')
described_class.configure_rspec
end
it 'does not run against production' do
......@@ -239,7 +234,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'without CI_PROJECT_NAME set' do
before do
stub_env('CI_PROJECT_NAME', nil)
described_class.configure_rspec
end
it 'runs on any pipeline' 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
before do
stub_env('CI_PROJECT_NAME', 'gitlab-qa')
described_class.configure_rspec
end
it 'runs on default branch pipelines' do
......@@ -310,7 +303,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with CI_PROJECT_NAME set' do
before do
stub_env('CI_PROJECT_NAME', 'NIGHTLY')
described_class.configure_rspec
end
it 'runs on designated pipeline' do
......@@ -353,7 +345,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'without CI_JOB_NAME set' do
before do
stub_env('CI_JOB_NAME', nil)
described_class.configure_rspec
end
context 'when excluding contexts' do
......@@ -396,7 +387,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
context 'with CI_JOB_NAME set' do
before do
stub_env('CI_JOB_NAME', 'ee:instance-image')
described_class.configure_rspec
end
context 'when excluding contexts' do
......
......@@ -2,9 +2,14 @@
require 'rspec/core/sandbox'
RSpec.configure do |c|
c.around do |ex|
RSpec.describe QA::Specs::Helpers::Quarantine do
include Helpers::StubENV
include QA::Specs::Helpers::RSpec
around do |ex|
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
# does not get a reference to the outer example (the real spec) if it calls
# something like `pending`
......@@ -15,18 +20,9 @@ RSpec.configure do |c|
ex.run
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
context 'with no tag focused' do
before do
described_class.configure_rspec
end
it 'skips before hooks of quarantined contexts' do
executed_hooks = []
......@@ -66,7 +62,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine focused' do
before do
described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :quarantine
end
......@@ -110,10 +105,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
describe '.skip_or_run_quarantined_tests_or_contexts' do
context 'with no tag focused' do
before do
described_class.configure_rspec
end
it 'skips quarantined tests' do
group = describe_successfully do
it('is pending', :quarantine) {}
......@@ -135,7 +126,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with environment set' do
before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com')
described_class.configure_rspec
end
context 'no pipeline specified' do
......@@ -168,7 +158,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
shared_examples 'skipped in project' do |project|
before do
stub_env('CI_PROJECT_NAME', project)
described_class.configure_rspec
end
it "is skipped in #{project}" do
......@@ -209,7 +198,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine focused' do
before do
described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :quarantine
end
......@@ -234,7 +222,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with a non-quarantine tag focused' do
before do
described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :foo
end
......@@ -277,7 +264,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do
context 'with :quarantine and non-quarantine tags focused' do
before do
described_class.configure_rspec
RSpec.configure do |c|
c.filter_run :foo, :bar, :quarantine
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
expect(response).not_to render_template('new')
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
......
......@@ -585,6 +585,26 @@ RSpec.describe 'Pipelines', :js do
expect(page).to have_selector('.gl-pagination .page-link', count: 4)
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
describe 'GET /:project/-/pipelines/show' do
......
......@@ -28,6 +28,7 @@ describe('Pipeline Url Component', () => {
flags: {},
},
pipelineScheduleUrl: 'foo',
pipelineKey: 'id',
};
const createComponent = (props) => {
......
......@@ -74,6 +74,7 @@ describe('Pipelines', () => {
const findTablePagination = () => wrapper.findComponent(TablePagination);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
const findPipelineKeyDropdown = () => wrapper.findByTestId('pipeline-key-dropdown');
const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
......@@ -528,6 +529,10 @@ describe('Pipelines', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
it('renders the pipeline key dropdown', () => {
expect(findPipelineKeyDropdown().exists()).toBe(true);
});
it('renders tab empty state finished scope', async () => {
mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
pipelines: [],
......@@ -623,6 +628,10 @@ describe('Pipelines', () => {
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', () => {
expect(findNavigationTabs().exists()).toBe(false);
expect(findTab('all').exists()).toBe(false);
......
......@@ -8,6 +8,7 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import { PipelineKeyOptions } from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
......@@ -24,6 +25,7 @@ describe('Pipelines Table', () => {
const defaultProps = {
pipelines: [],
viewType: 'root',
pipelineKeyOption: PipelineKeyOptions[0],
};
const createMockPipeline = () => {
......@@ -80,7 +82,7 @@ describe('Pipelines Table', () => {
it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline');
expect(findPipelineTh().text()).toBe('Pipeline ID');
expect(findTriggererTh().text()).toBe('Triggerer');
expect(findCommitTh().text()).toBe('Commit');
expect(findStagesTh().text()).toBe('Stages');
......
......@@ -144,23 +144,6 @@ describe('RelatedIssuableItem', () => {
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', () => {
......
......@@ -13,12 +13,13 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
let(:vulnerabilities) { table(:vulnerabilities) }
let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
let(:vulnerability_identifier) do
vulnerability_identifiers.create!(
project_id: project.id,
external_type: 'uuid-v5',
external_id: 'uuid-v5',
fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
fingerprint: Gitlab::Database::ShaAttribute.serialize('7e394d1b1eb461a7406d7b1e08f057a1cf11287a'),
name: 'Identifier for UUIDv5')
end
......@@ -27,7 +28,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
project_id: project.id,
external_type: 'uuid-v4',
external_id: 'uuid-v4',
fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89',
fingerprint: Gitlab::Database::ShaAttribute.serialize('772da93d34a1ba010bcb5efa9fb6f8e01bafcc89'),
name: 'Identifier for UUIDv4')
end
......@@ -59,7 +60,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: different_scanner.id,
primary_identifier_id: different_vulnerability_identifier.id,
report_type: 0, # "sast"
location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e",
location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4
)
end
......@@ -91,7 +92,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: scanner.id,
primary_identifier_id: vulnerability_identifier.id,
report_type: 0, # "sast"
location_fingerprint: "838574be0210968bf6b9f569df9c2576242cbf0a",
location_fingerprint: Gitlab::Database::ShaAttribute.serialize("838574be0210968bf6b9f569df9c2576242cbf0a"),
uuid: known_uuid_v5
)
end
......@@ -115,7 +116,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
scanner_id: different_scanner.id,
primary_identifier_id: different_vulnerability_identifier.id,
report_type: 0, # "sast"
location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e",
location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
uuid: known_uuid_v4
)
......
......@@ -87,12 +87,18 @@ RSpec.describe Gitlab::EtagCaching::Router::Restful do
end
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.name).to eq 'environments'
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
result = match_route('/my-group/my-project/-/pipelines/2.json')
......
......@@ -118,6 +118,24 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject[:redis_hll_counters][:quickactions].keys).to include(*known_events_keys)
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
expect(subject[:redis_hll_counters][:issues_edit].keys).to include(
: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