Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-15 21:09:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-15 21:09:50 +0300
commitb302502690b2e1422a4ef6abebdcf3ff4dc88543 (patch)
tree0cd7ca6ff21b59c007277b5bae9611a7cedd8134
parentb616fd825faac3e7f194e1f942ef30730021e463 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/performance/block_given_with_explicit_block.yml7
-rw-r--r--.rubocop_todo/performance/collection_literal_in_loop.yml7
-rw-r--r--.rubocop_todo/performance/constant_regexp.yml5
-rw-r--r--.rubocop_todo/performance/method_object_as_block.yml7
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue2
-rw-r--r--app/assets/javascripts/issues/show/index.js1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/performance_insights_modal.vue168
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql28
-rw-r--r--app/assets/javascripts/pipelines/utils.js21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue2
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue2
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb16
-rw-r--r--app/controllers/projects/incidents_controller.rb3
-rw-r--r--app/models/hooks/web_hook.rb30
-rw-r--r--app/services/web_hook_service.rb16
-rw-r--r--app/views/layouts/_flash.html.haml3
-rw-r--r--app/views/projects/imports/show.html.haml5
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml4
-rw-r--r--config/feature_flags/development/ci_minimal_cost_factor_for_gitlab_contributors.yml8
-rw-r--r--config/feature_flags/ops/ci_minimal_cost_factor_for_gitlab_namespaces.yml8
-rw-r--r--config/routes/user.rb2
-rw-r--r--db/migrate/20220703182234_add_findings_partition_number_to_security_scans.rb9
-rw-r--r--db/migrate/20220703182254_add_partition_number_to_security_findings.rb9
-rw-r--r--db/post_migrate/20220703182314_add_check_constraint_for_security_findings_partition_number.rb15
-rw-r--r--db/post_migrate/20220706122719_create_index_on_security_findings_uuid_id_desc.rb23
-rw-r--r--db/post_migrate/20220714122311_add_async_index_on_security_findings_id_and_partition_number.rb15
-rw-r--r--db/post_migrate/20220714122418_add_async_index_on_security_findings_unique_columns.rb15
-rw-r--r--db/post_migrate/20220715054506_add_parent_link_unique_work_item_index.rb19
-rw-r--r--db/schema_migrations/202207031822341
-rw-r--r--db/schema_migrations/202207031822541
-rw-r--r--db/schema_migrations/202207031823141
-rw-r--r--db/schema_migrations/202207061227191
-rw-r--r--db/schema_migrations/202207141223111
-rw-r--r--db/schema_migrations/202207141224181
-rw-r--r--db/schema_migrations/202207150545061
-rw-r--r--db/structure.sql13
-rw-r--r--doc/administration/audit_event_streaming.md11
-rw-r--r--doc/ci/pipelines/settings.md2
-rw-r--r--doc/ci/yaml/artifacts_reports.md10
-rw-r--r--doc/development/integrations/secure.md2
-rw-r--r--doc/operations/incident_management/img/timeline_events_v15_1.pngbin0 -> 36994 bytes
-rw-r--r--doc/operations/incident_management/incidents.md61
-rw-r--r--doc/security/two_factor_authentication.md8
-rw-r--r--doc/update/index.md1
-rw-r--r--doc/user/application_security/index.md2
-rw-r--r--doc/user/application_security/security_dashboard/index.md51
-rw-r--r--doc/user/application_security/vulnerability_report/pipeline.md57
-rw-r--r--doc/user/clusters/agent/install/index.md6
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/milestones/burndown_and_burnup_charts.md2
-rw-r--r--doc/user/project/milestones/img/milestones_promote_milestone.pngbin49288 -> 0 bytes
-rw-r--r--doc/user/project/milestones/index.md110
-rw-r--r--doc/user/tasks.md2
-rw-r--r--lib/gitlab/ci/runner_upgrade_check.rb14
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb9
-rw-r--r--locale/gitlab.pot46
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json82
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js12
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js59
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js242
-rw-r--r--spec/frontend/pipelines/performance_insights_modal_spec.js122
-rw-r--r--spec/frontend/pipelines/utils_spec.js44
-rw-r--r--spec/lib/gitlab/ci/runner_upgrade_check_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb59
-rw-r--r--spec/models/hooks/web_hook_spec.rb76
-rw-r--r--spec/routing/routing_spec.rb20
-rw-r--r--spec/services/web_hook_service_spec.rb68
-rw-r--r--spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb3
-rw-r--r--spec/views/layouts/_flash.html.haml_spec.rb14
80 files changed, 1578 insertions, 177 deletions
diff --git a/.rubocop_todo/performance/block_given_with_explicit_block.yml b/.rubocop_todo/performance/block_given_with_explicit_block.yml
index ff1f5e568ea..b919dbd19e6 100644
--- a/.rubocop_todo/performance/block_given_with_explicit_block.yml
+++ b/.rubocop_todo/performance/block_given_with_explicit_block.yml
@@ -1,9 +1,6 @@
---
# Cop supports --auto-correct.
Performance/BlockGivenWithExplicitBlock:
- # Offense count: 53
- # Temporarily disabled due to too many offenses
- Enabled: false
Exclude:
- 'app/controllers/concerns/redis_tracking.rb'
- 'app/helpers/badges_helper.rb'
@@ -26,11 +23,14 @@ Performance/BlockGivenWithExplicitBlock:
- 'lib/gitlab/metrics/methods/metric_options.rb'
- 'lib/gitlab/null_request_store.rb'
- 'lib/gitlab/quick_actions/dsl.rb'
+ - 'lib/gitlab/redis/multi_store.rb'
- 'lib/gitlab/safe_request_loader.rb'
- 'lib/gitlab/search/query.rb'
- 'lib/gitlab/string_placeholder_replacer.rb'
- 'lib/gitlab/terraform/state_migration_helper.rb'
+ - 'lib/gitlab/usage/metrics/instrumentations/base_metric.rb'
- 'lib/gitlab/usage/metrics/instrumentations/database_metric.rb'
+ - 'lib/gitlab/usage/metrics/instrumentations/numbers_metric.rb'
- 'lib/gitlab/usage_data_queries.rb'
- 'lib/gitlab/utils/usage_data.rb'
- 'qa/qa/page/view.rb'
@@ -38,5 +38,6 @@ Performance/BlockGivenWithExplicitBlock:
- 'spec/lib/gitlab/slash_commands/deploy_spec.rb'
- 'spec/support/helpers/graphql_helpers.rb'
- 'spec/support/helpers/query_recorder.rb'
+ - 'spec/support/helpers/stub_method_calls.rb'
- 'tooling/lib/tooling/helm3_client.rb'
- 'tooling/lib/tooling/test_map_packer.rb'
diff --git a/.rubocop_todo/performance/collection_literal_in_loop.yml b/.rubocop_todo/performance/collection_literal_in_loop.yml
index 4b012bf6645..50fd75827fb 100644
--- a/.rubocop_todo/performance/collection_literal_in_loop.yml
+++ b/.rubocop_todo/performance/collection_literal_in_loop.yml
@@ -1,12 +1,8 @@
---
Performance/CollectionLiteralInLoop:
- # Offense count: 45
- # Temporarily disabled due to too many offenses
- Enabled: false
Exclude:
- 'config/application.rb'
- 'config/initializers/1_settings.rb'
- - 'ee/app/models/ee/merge_request.rb'
- 'ee/spec/features/admin/admin_settings_spec.rb'
- 'ee/spec/support/shared_examples/features/protected_branches_access_control_shared_examples.rb'
- 'ee/spec/workers/app_sec/dast/profile_schedule_worker_spec.rb'
@@ -20,15 +16,18 @@ Performance/CollectionLiteralInLoop:
- 'lib/tasks/gitlab/seed/group_seed.rake'
- 'spec/bin/sidekiq_cluster_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
+ - 'spec/finders/ci/runners_finder_spec.rb'
- 'spec/lib/banzai/reference_parser/base_parser_spec.rb'
- 'spec/lib/gitlab/database/migration_helpers/restrict_gitlab_schema_spec.rb'
- 'spec/lib/gitlab/file_detector_spec.rb'
- 'spec/lib/gitlab/search/abuse_detection_spec.rb'
- 'spec/lib/gitlab/utils/markdown_spec.rb'
+ - 'spec/metrics_server/metrics_server_spec.rb'
- 'spec/models/analytics/cycle_analytics/aggregation_spec.rb'
- 'spec/models/ci/build_spec.rb'
- 'spec/models/ci/pipeline_spec.rb'
- 'spec/models/namespace_statistics_spec.rb'
+ - 'spec/models/project_spec.rb'
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
- 'spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb'
- 'spec/presenters/packages/nuget/service_index_presenter_spec.rb'
diff --git a/.rubocop_todo/performance/constant_regexp.yml b/.rubocop_todo/performance/constant_regexp.yml
index 4ed40db498f..23d03ced8d7 100644
--- a/.rubocop_todo/performance/constant_regexp.yml
+++ b/.rubocop_todo/performance/constant_regexp.yml
@@ -1,15 +1,13 @@
---
# Cop supports --auto-correct.
Performance/ConstantRegexp:
- # Offense count: 46
- # Temporarily disabled due to too many offenses
- Enabled: false
Exclude:
- 'app/models/commit.rb'
- 'app/models/commit_range.rb'
- 'app/models/custom_emoji.rb'
- 'app/models/gpg_key.rb'
- 'app/models/merge_request.rb'
+ - 'app/models/packages/package.rb'
- 'app/models/project.rb'
- 'app/models/wiki.rb'
- 'ee/app/models/ee/epic.rb'
@@ -27,5 +25,6 @@ Performance/ConstantRegexp:
- 'lib/gitlab/regex.rb'
- 'scripts/perf/query_limiting_report.rb'
- 'scripts/validate_migration_schema'
+ - 'spec/features/users/email_verification_on_login_spec.rb'
- 'spec/models/concerns/token_authenticatable_spec.rb'
- 'spec/services/notes/copy_service_spec.rb'
diff --git a/.rubocop_todo/performance/method_object_as_block.yml b/.rubocop_todo/performance/method_object_as_block.yml
index 265e03073db..1bc82ff05ec 100644
--- a/.rubocop_todo/performance/method_object_as_block.yml
+++ b/.rubocop_todo/performance/method_object_as_block.yml
@@ -1,8 +1,5 @@
---
Performance/MethodObjectAsBlock:
- # Offense count: 150
- # Temporarily disabled due to too many offenses
- Enabled: false
Exclude:
- 'app/controllers/concerns/metrics_dashboard.rb'
- 'app/controllers/concerns/requires_whitelisted_monitoring_client.rb'
@@ -30,7 +27,6 @@ Performance/MethodObjectAsBlock:
- 'ee/app/graphql/resolvers/vulnerabilities/scanners_resolver.rb'
- 'ee/app/services/dashboard/projects/create_service.rb'
- 'ee/app/services/security/findings/cleanup_service.rb'
- - 'ee/app/services/security/ingestion/bulk_insertable_task.rb'
- 'ee/app/services/security/ingestion/ingest_reports_service.rb'
- 'ee/app/services/security/ingestion/tasks/ingest_vulnerability_statistics.rb'
- 'ee/app/services/security/store_findings_metadata_service.rb'
@@ -38,6 +34,7 @@ Performance/MethodObjectAsBlock:
- 'ee/lib/ee/container_registry/client.rb'
- 'ee/lib/ee/gitlab/ci/config_ee.rb'
- 'ee/lib/ee/gitlab/etag_caching/router/rails.rb'
+ - 'ee/lib/gitlab/ingestion/bulk_insertable_task.rb'
- 'ee/spec/services/groups/participants_service_spec.rb'
- 'lib/api/helpers/packages/conan/api_helpers.rb'
- 'lib/bulk_imports/pipeline.rb'
@@ -80,6 +77,7 @@ Performance/MethodObjectAsBlock:
- 'lib/gitlab/utils.rb'
- 'lib/peek/views/detailed_view.rb'
- 'lib/tasks/gitlab/assets.rake'
+ - 'lib/unnested_in_filters/rewriter.rb'
- 'qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb'
- 'rubocop/cop/avoid_return_from_blocks.rb'
- 'rubocop/cop/gitlab/mark_used_feature_flags.rb'
@@ -95,5 +93,6 @@ Performance/MethodObjectAsBlock:
- 'spec/support/helpers/migrations_helpers.rb'
- 'spec/support/shared_examples/models/active_record_enum_shared_examples.rb'
- 'spec/support_specs/helpers/stub_feature_flags_spec.rb'
+ - 'tooling/lib/tooling/find_codeowners.rb'
- 'tooling/lib/tooling/test_map_packer.rb'
- 'tooling/quality/test_level.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index bf5145be7ee..506e6d1e57d 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1250b121b00ef5b3d637463cd4b9e5d93076f9b0
+2ba30c8b1b5a428a645faf72881771af9a505ab2
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index bb120e876c6..91951fd8ad7 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.59.0
+1.61.0
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 7dd9dc4a01f..449da394841 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -465,7 +465,7 @@ export default {
},
handleDeleteTask(description) {
this.$emit('updateDescription', description);
- this.$toast.show(s__('WorkItem|Work item deleted'));
+ this.$toast.show(s__('WorkItem|Task deleted'));
},
updateWorkItemIdUrlQuery(workItemId) {
updateHistory({
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 5bdad010af7..459a3804837 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -63,6 +63,7 @@ export function initIncidentApp(issueData = {}) {
return createElement(IssueApp, {
props: {
...issueData,
+ issueId: Number(issuableId),
issuableStatus: state,
descriptionComponent: IncidentTabs,
showTitleBorder: false,
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index f822e2c0874..14872c34afb 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -281,6 +281,7 @@ export default {
:type="graphViewType"
:show-links="showLinks"
:tip-previously-dismissed="hoverTipPreviouslyDismissed"
+ :is-pipeline-complete="pipeline.complete"
@dismissHoverTip="handleTipDismissal"
@updateViewType="updateViewType"
@updateShowLinksState="updateShowLinksState"
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
index 1920fed84ec..a8c5d85f4ed 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
@@ -1,17 +1,33 @@
<script>
-import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlButton,
+ GlButtonGroup,
+ GlLoadingIcon,
+ GlToggle,
+ GlModalDirective,
+} from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import Tracking from '~/tracking';
+import PerformanceInsightsModal from '../performance_insights_modal.vue';
+import { performanceModalId } from '../../constants';
import { STAGE_VIEW, LAYER_VIEW } from './constants';
export default {
name: 'GraphViewSelector',
+ performanceModalId,
components: {
GlAlert,
GlButton,
GlButtonGroup,
GlLoadingIcon,
GlToggle,
+ PerformanceInsightsModal,
},
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ mixins: [Tracking.mixin()],
props: {
showLinks: {
type: Boolean,
@@ -25,6 +41,10 @@ export default {
type: String,
required: true,
},
+ isPipelineComplete: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -39,6 +59,7 @@ export default {
hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'),
linksLabelText: s__('GraphViewType|Show dependencies'),
viewLabelText: __('Group jobs by'),
+ performanceBtnText: __('Performance insights'),
},
views: {
[STAGE_VIEW]: {
@@ -129,6 +150,9 @@ export default {
this.$emit('updateShowLinksState', val);
});
},
+ trackInsightsClick() {
+ this.track('click_insights_button', { label: 'performance_insights' });
+ },
},
};
</script>
@@ -154,6 +178,15 @@ export default {
</gl-button>
</gl-button-group>
+ <gl-button
+ v-if="isPipelineComplete"
+ v-gl-modal="$options.performanceModalId"
+ data-testid="pipeline-insights-btn"
+ @click="trackInsightsClick"
+ >
+ {{ $options.i18n.performanceBtnText }}
+ </gl-button>
+
<div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
<gl-toggle
v-model="showLinksActive"
@@ -169,5 +202,7 @@ export default {
<gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip">
{{ $options.i18n.hoverTipText }}
</gl-alert>
+
+ <performance-insights-modal />
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/performance_insights_modal.vue b/app/assets/javascripts/pipelines/components/performance_insights_modal.vue
new file mode 100644
index 00000000000..ae6b9186930
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/performance_insights_modal.vue
@@ -0,0 +1,168 @@
+<script>
+import { GlAlert, GlCard, GlLink, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { humanizeTimeInterval } from '~/lib/utils/datetime_utility';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import getPerformanceInsightsQuery from '../graphql/queries/get_performance_insights.query.graphql';
+import { performanceModalId } from '../constants';
+import { calculateJobStats, calculateSlowestFiveJobs } from '../utils';
+
+export default {
+ name: 'PerformanceInsightsModal',
+ i18n: {
+ queuedCardHeader: s__('Pipeline|Longest queued job'),
+ queuedCardHelp: s__(
+ 'Pipeline|The longest queued job is the job that spent the longest time in the pending state, waiting to be picked up by a Runner',
+ ),
+ executedCardHeader: s__('Pipeline|Last executed job'),
+ executedCardHelp: s__(
+ 'Pipeline|The last executed job is the last job to start in the pipeline.',
+ ),
+ viewDependency: s__('Pipeline|View dependency'),
+ slowJobsTitle: s__('Pipeline|Five slowest jobs'),
+ feeback: __('Feedback issue'),
+ insightsLimit: s__('Pipeline|Only able to show first 100 results'),
+ },
+ modal: {
+ title: s__('Pipeline|Performance insights'),
+ actionCancel: {
+ text: __('Close'),
+ attributes: {
+ variant: 'confirm',
+ },
+ },
+ },
+ performanceModalId,
+ components: {
+ GlAlert,
+ GlCard,
+ GlLink,
+ GlModal,
+ GlLoadingIcon,
+ HelpPopover,
+ },
+ inject: {
+ pipelineIid: {
+ default: '',
+ },
+ pipelineProjectPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ jobs: {
+ query: getPerformanceInsightsQuery,
+ variables() {
+ return {
+ fullPath: this.pipelineProjectPath,
+ iid: this.pipelineIid,
+ };
+ },
+ update(data) {
+ return data.project?.pipeline?.jobs;
+ },
+ },
+ },
+ data() {
+ return {
+ jobs: null,
+ };
+ },
+ computed: {
+ longestQueuedJob() {
+ return calculateJobStats(this.jobs, 'queuedDuration');
+ },
+ lastExecutedJob() {
+ return calculateJobStats(this.jobs, 'startedAt');
+ },
+ slowestFiveJobs() {
+ return calculateSlowestFiveJobs(this.jobs);
+ },
+ queuedDurationDisplay() {
+ return humanizeTimeInterval(this.longestQueuedJob.queuedDuration);
+ },
+ showLimitMessage() {
+ return this.jobs.pageInfo.hasNextPage;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :modal-id="$options.performanceModalId"
+ :title="$options.modal.title"
+ :action-cancel="$options.modal.actionCancel"
+ >
+ <gl-loading-icon v-if="$apollo.queries.jobs.loading" size="lg" />
+
+ <template v-else>
+ <gl-alert v-if="showLimitMessage" class="gl-mb-4" :dismissible="false">
+ <p>{{ $options.i18n.insightsLimit }}</p>
+ <gl-link href="https://gitlab.com/gitlab-org/gitlab/-/issues/365902" class="gl-mt-5">
+ {{ $options.i18n.feeback }}
+ </gl-link>
+ </gl-alert>
+ <div class="gl-display-flex gl-justify-content-space-between gl-mb-7">
+ <gl-card class="gl-w-half gl-mr-7 gl-text-center">
+ <template #header>
+ <span class="gl-font-weight-bold">{{ $options.i18n.queuedCardHeader }}</span>
+ <help-popover>
+ {{ $options.i18n.queuedCardHelp }}
+ </help-popover>
+ </template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <span
+ class="gl-font-weight-bold gl-font-size-h2 gl-mb-2"
+ data-testid="insights-queued-card-data"
+ >
+ {{ queuedDurationDisplay }}
+ </span>
+ <gl-link
+ :href="longestQueuedJob.detailedStatus.detailsPath"
+ data-testid="insights-queued-card-link"
+ >
+ {{ longestQueuedJob.name }}
+ </gl-link>
+ </div>
+ </gl-card>
+ <gl-card class="gl-w-half gl-text-center" data-testid="insights-executed-card">
+ <template #header>
+ <span class="gl-font-weight-bold">{{ $options.i18n.executedCardHeader }}</span>
+ <help-popover>
+ {{ $options.i18n.executedCardHelp }}
+ </help-popover>
+ </template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <span
+ class="gl-font-weight-bold gl-font-size-h2 gl-mb-2"
+ data-testid="insights-executed-card-data"
+ >
+ {{ lastExecutedJob.name }}
+ </span>
+ <gl-link
+ :href="lastExecutedJob.detailedStatus.detailsPath"
+ data-testid="insights-executed-card-link"
+ >
+ {{ $options.i18n.viewDependency }}
+ </gl-link>
+ </div>
+ </gl-card>
+ </div>
+
+ <div class="gl-mt-7">
+ <span class="gl-font-weight-bold">{{ $options.i18n.slowJobsTitle }}</span>
+ <div
+ v-for="job in slowestFiveJobs"
+ :key="job.name"
+ class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-3 gl-p-4 gl-border-t-1 gl-border-t-solid gl-border-b-0 gl-border-b-solid gl-border-gray-100"
+ >
+ <span data-testid="insights-slow-job-stage">{{ job.stage.name }}</span>
+ <gl-link :href="job.detailedStatus.detailsPath" data-testid="insights-slow-job-link">{{
+ job.name
+ }}</gl-link>
+ </div>
+ </div>
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 0510992e962..2e825016c91 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -109,3 +109,5 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-20p',
},
];
+
+export const performanceModalId = 'performanceInsightsModal';
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql
new file mode 100644
index 00000000000..25e990c8934
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql
@@ -0,0 +1,28 @@
+query getPerformanceInsightsData($fullPath: ID!, $iid: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ pipeline(iid: $iid) {
+ id
+ jobs {
+ pageInfo {
+ hasNextPage
+ }
+ nodes {
+ id
+ duration
+ detailedStatus {
+ id
+ detailsPath
+ }
+ name
+ stage {
+ id
+ name
+ }
+ startedAt
+ queuedDuration
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 588d15495ab..83e00b80426 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -153,3 +153,24 @@ export const getPipelineDefaultTab = (url) => {
return null;
};
+
+export const calculateJobStats = (jobs, sortField) => {
+ const jobNodes = [...jobs.nodes];
+
+ const sorted = jobNodes.sort((a, b) => {
+ return b[sortField] - a[sortField];
+ });
+
+ return sorted[0];
+};
+
+export const calculateSlowestFiveJobs = (jobs) => {
+ const jobNodes = [...jobs.nodes];
+ const limit = 5;
+
+ return jobNodes
+ .sort((a, b) => {
+ return b.duration - a.duration;
+ })
+ .slice(0, limit);
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 39a374d1193..59767eb2e6e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -221,6 +221,17 @@ export default {
>
{{ __('Rebase') }}
</gl-button>
+ <gl-button
+ v-if="glFeatures.restructuredMrWidget && showRebaseWithoutCi"
+ :loading="isMakingRequest"
+ variant="confirm"
+ size="small"
+ category="secondary"
+ data-testid="rebase-without-ci-button"
+ @click="rebaseWithoutCi"
+ >
+ {{ __('Rebase without pipeline') }}
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 31e4a932c5a..77002eeaf55 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -5,7 +5,7 @@ import Tracking from '~/tracking';
export default {
i18n: {
- deleteWorkItem: s__('WorkItem|Delete work item'),
+ deleteTask: s__('WorkItem|Delete task'),
},
components: {
GlDropdown,
@@ -54,7 +54,7 @@ export default {
right
>
<gl-dropdown-item v-gl-modal="'work-item-confirm-delete'">{{
- $options.i18n.deleteWorkItem
+ $options.i18n.deleteTask
}}</gl-dropdown-item>
</gl-dropdown>
<gl-modal
@@ -66,9 +66,7 @@ export default {
@hide="handleCancelDeleteWorkItem"
>
{{
- s__(
- 'WorkItem|Are you sure you want to delete the work item? This action cannot be reversed.',
- )
+ s__('WorkItem|Are you sure you want to delete the task? This action cannot be reversed.')
}}
</gl-modal>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index b4f9e38a64a..df7c6cab7ef 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -80,7 +80,7 @@ export default {
.catch((e) => {
this.error =
e.message ||
- s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+ s__('WorkItem|Something went wrong when deleting the task. Please try again.');
});
},
closeModal() {
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 0047b03108a..482da5419c6 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -10,7 +10,7 @@ import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.
import ItemTitle from '../components/item_title.vue';
export default {
- createErrorText: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
+ createErrorText: s__('WorkItem|Something went wrong when creating a task. Please try again'),
fetchTypesErrorText: s__(
'WorkItem|Something went wrong when fetching work item types. Please try again',
),
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
index 6aa46b8e4c3..955dfe58449 100644
--- a/app/controllers/ldap/omniauth_callbacks_controller.rb
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -3,10 +3,12 @@
class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
extend ::Gitlab::Utils::Override
+ before_action :check_action_name_in_available_providers
+
def self.define_providers!
return unless Gitlab::Auth::Ldap::Config.sign_in_enabled?
- Gitlab::Auth::Ldap::Config.available_servers.each do |server|
+ Gitlab::Auth::Ldap::Config.servers.each do |server|
alias_method server['provider_name'], :ldap
end
end
@@ -36,6 +38,18 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
redirect_to new_user_session_path
end
+
+ private
+
+ def check_action_name_in_available_providers
+ render_404 unless available_providers.include?(action_name)
+ end
+
+ def available_providers
+ Gitlab::Auth::Ldap::Config.available_servers.map do |server|
+ server['provider_name']
+ end
+ end
end
Ldap::OmniauthCallbacksController.prepend_mod_with('Ldap::OmniauthCallbacksController')
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 70eab792b40..f9fa8046962 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -8,6 +8,9 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :load_incident, only: [:show]
before_action do
push_frontend_feature_flag(:incident_timeline, @project)
+ push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
+ push_frontend_feature_flag(:work_items_mvc_2)
+ push_frontend_feature_flag(:work_items_hierarchy, @project)
end
feature_category :incident_management
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 37fd612e652..71e7206718e 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -3,6 +3,8 @@
class WebHook < ApplicationRecord
include Sortable
+ InterpolationError = Class.new(StandardError)
+
MAX_FAILURES = 100
FAILURE_THRESHOLD = 3 # three strikes
INITIAL_BACKOFF = 10.minutes
@@ -36,6 +38,7 @@ class WebHook < ApplicationRecord
validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true
validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' }
+ validate :no_missing_url_variables
after_initialize :initialize_url_variables
@@ -164,6 +167,20 @@ class WebHook < ApplicationRecord
super(options)
end
+ # See app/validators/json_schemas/web_hooks_url_variables.json
+ VARIABLE_REFERENCE_RE = /\{([A-Za-z_][A-Za-z0-9_]+)\}/.freeze
+
+ def interpolated_url
+ return url unless url.include?('{')
+
+ vars = url_variables
+ url.gsub(VARIABLE_REFERENCE_RE) do
+ vars.fetch(_1.delete_prefix('{').delete_suffix('}'))
+ end
+ rescue KeyError => e
+ raise InterpolationError, "Invalid URL template. Missing key #{e.key}"
+ end
+
private
def web_hooks_disable_failed?
@@ -177,4 +194,17 @@ class WebHook < ApplicationRecord
def rate_limiter
@rate_limiter ||= Gitlab::WebHooks::RateLimiter.new(self)
end
+
+ def no_missing_url_variables
+ return if url.nil?
+
+ variable_names = url_variables.keys
+ used_variables = url.scan(VARIABLE_REFERENCE_RE).map(&:first)
+
+ missing = used_variables - variable_names
+
+ return if missing.empty?
+
+ errors.add(:url, "Invalid URL template. Missing keys: #{missing}")
+ end
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index b68269fcf72..cd2c7402713 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -69,7 +69,7 @@ class WebHookService
start_time = Gitlab::Metrics::System.monotonic_time
response = if parsed_url.userinfo.blank?
- make_request(hook.url)
+ make_request(parsed_url.to_s)
else
make_request_with_auth
end
@@ -87,17 +87,19 @@ class WebHookService
rescue *Gitlab::HTTP::HTTP_ERRORS,
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e
execution_duration = Gitlab::Metrics::System.monotonic_time - start_time
+ error_message = e.to_s
+
log_execution(
response: InternalErrorResponse.new,
execution_duration: execution_duration,
- error_message: e.to_s
+ error_message: error_message
)
Gitlab::AppLogger.error("WebHook Error after #{execution_duration.to_i.seconds}s => #{e}")
{
status: :error,
- message: e.to_s
+ message: error_message
}
end
@@ -117,7 +119,11 @@ class WebHookService
private
def parsed_url
- @parsed_url ||= URI.parse(hook.url)
+ @parsed_url ||= URI.parse(hook.interpolated_url)
+ rescue WebHook::InterpolationError => e
+ # Behavior-preserving fallback.
+ Gitlab::ErrorTracking.track_exception(e)
+ @parsed_url = URI.parse(hook.url)
end
def make_request(url, basic_auth = false)
@@ -130,7 +136,7 @@ class WebHookService
end
def make_request_with_auth
- post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
+ post_url = parsed_url.to_s.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password.presence || '')
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 21cccb86398..ab4b3cf6afd 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,6 +1,7 @@
-# We currently only support `alert`, `notice`, `success`, 'toast', and 'raw'
- icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'}
- type_to_variant = {'alert' => 'danger', 'notice' => 'info', 'success' => 'success'}
+- closable = %w[alert notice success]
.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } }
- flash.each do |key, value|
- if key == 'toast' && value
@@ -13,6 +14,6 @@
%div{ class: "flash-#{key} mb-2", data: { testid: "alert-#{type_to_variant[key]}" } }
= sprite_icon(icons[key], css_class: 'align-middle mr-1') unless icons[key].nil?
%span= value
- - if %w(alert notice success).include?(key)
+ - if closable.include?(key)
%div{ class: "close-icon-wrapper js-close-icon" }
= sprite_icon('close', css_class: 'close-icon gl-vertical-align-baseline!')
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 8096bc6cead..9fe541c5912 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -3,9 +3,10 @@
.save-project-loader
.center
- %h2
+ %h2.gl--flex-center.gl-flex-direction-column.gl-sm-flex-direction-row
= gl_loading_icon(inline: true)
- = import_in_progress_title
+ %span.gl-ml-3
+ = import_in_progress_title
- if !has_ci_cd_only_params? && @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
%p
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index a0810cfe37d..6b367c735c3 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -26,7 +26,7 @@
.gl-new-dropdown-item-text-wrapper
= _('Close')
= display_issuable_type
- - elsif !@merge_request.source_project_missing?
+ - elsif !@merge_request.source_project_missing? && @merge_request.closed?
%li.gl-new-dropdown-item
= link_to reopen_issuable_path(@merge_request), method: :put, class: 'dropdown-item' do
.gl-new-dropdown-item-text-wrapper
@@ -34,7 +34,7 @@
= display_issuable_type
- unless current_controller?('conflicts')
- - if current_user && moved_mr_sidebar_enabled?
+ - if current_user && moved_mr_sidebar_enabled? && !@merge_request.merged?
%li.gl-new-dropdown-divider
%hr.dropdown-divider
%li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
diff --git a/config/feature_flags/development/ci_minimal_cost_factor_for_gitlab_contributors.yml b/config/feature_flags/development/ci_minimal_cost_factor_for_gitlab_contributors.yml
new file mode 100644
index 00000000000..e571abdc97f
--- /dev/null
+++ b/config/feature_flags/development/ci_minimal_cost_factor_for_gitlab_contributors.yml
@@ -0,0 +1,8 @@
+---
+name: ci_minimal_cost_factor_for_gitlab_contributors
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89742
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365862
+milestone: '15.2'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/ops/ci_minimal_cost_factor_for_gitlab_namespaces.yml b/config/feature_flags/ops/ci_minimal_cost_factor_for_gitlab_namespaces.yml
new file mode 100644
index 00000000000..fbbd4d9672b
--- /dev/null
+++ b/config/feature_flags/ops/ci_minimal_cost_factor_for_gitlab_namespaces.yml
@@ -0,0 +1,8 @@
+---
+name: ci_minimal_cost_factor_for_gitlab_namespaces
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89742
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367692
+milestone: '15.2'
+type: ops
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/routes/user.rb b/config/routes/user.rb
index a5fc53b61d2..96e8c850da4 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -15,7 +15,7 @@ end
# Use custom controller for LDAP omniauth callback
if Gitlab::Auth::Ldap::Config.sign_in_enabled?
devise_scope :user do
- Gitlab::Auth::Ldap::Config.available_servers.each do |server|
+ Gitlab::Auth::Ldap::Config.servers.each do |server|
override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks')
end
end
diff --git a/db/migrate/20220703182234_add_findings_partition_number_to_security_scans.rb b/db/migrate/20220703182234_add_findings_partition_number_to_security_scans.rb
new file mode 100644
index 00000000000..f7d50a6d5cf
--- /dev/null
+++ b/db/migrate/20220703182234_add_findings_partition_number_to_security_scans.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddFindingsPartitionNumberToSecurityScans < Gitlab::Database::Migration[2.0]
+ enable_lock_retries!
+
+ def change
+ add_column :security_scans, :findings_partition_number, :integer, default: 1, null: false
+ end
+end
diff --git a/db/migrate/20220703182254_add_partition_number_to_security_findings.rb b/db/migrate/20220703182254_add_partition_number_to_security_findings.rb
new file mode 100644
index 00000000000..85ff4f2eb7c
--- /dev/null
+++ b/db/migrate/20220703182254_add_partition_number_to_security_findings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddPartitionNumberToSecurityFindings < Gitlab::Database::Migration[2.0]
+ enable_lock_retries!
+
+ def change
+ add_column :security_findings, :partition_number, :integer, default: 1, null: false
+ end
+end
diff --git a/db/post_migrate/20220703182314_add_check_constraint_for_security_findings_partition_number.rb b/db/post_migrate/20220703182314_add_check_constraint_for_security_findings_partition_number.rb
new file mode 100644
index 00000000000..82b5d0b165d
--- /dev/null
+++ b/db/post_migrate/20220703182314_add_check_constraint_for_security_findings_partition_number.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddCheckConstraintForSecurityFindingsPartitionNumber < Gitlab::Database::Migration[2.0]
+ CONSTRAINT_NAME = 'check_partition_number'
+
+ disable_ddl_transaction!
+
+ def up
+ add_check_constraint(:security_findings, 'partition_number = 1', CONSTRAINT_NAME)
+ end
+
+ def down
+ remove_check_constraint(:security_findings, CONSTRAINT_NAME)
+ end
+end
diff --git a/db/post_migrate/20220706122719_create_index_on_security_findings_uuid_id_desc.rb b/db/post_migrate/20220706122719_create_index_on_security_findings_uuid_id_desc.rb
new file mode 100644
index 00000000000..0c2e2cb4b70
--- /dev/null
+++ b/db/post_migrate/20220706122719_create_index_on_security_findings_uuid_id_desc.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class CreateIndexOnSecurityFindingsUuidIdDesc < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_on_security_findings_uuid_and_id_order_desc'
+
+ def up
+ add_concurrent_index(
+ :security_findings,
+ %i[uuid id],
+ order: { id: :desc },
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(
+ :security_findings,
+ INDEX_NAME
+ )
+ end
+end
diff --git a/db/post_migrate/20220714122311_add_async_index_on_security_findings_id_and_partition_number.rb b/db/post_migrate/20220714122311_add_async_index_on_security_findings_id_and_partition_number.rb
new file mode 100644
index 00000000000..02d18ba8b40
--- /dev/null
+++ b/db/post_migrate/20220714122311_add_async_index_on_security_findings_id_and_partition_number.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddAsyncIndexOnSecurityFindingsIdAndPartitionNumber < Gitlab::Database::Migration[2.0]
+ INDEX_NAME = 'security_findings_partitioned_pkey'
+
+ disable_ddl_transaction!
+
+ def up
+ prepare_async_index :security_findings, [:id, :partition_number], unique: true, name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index_by_name :security_findings, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220714122418_add_async_index_on_security_findings_unique_columns.rb b/db/post_migrate/20220714122418_add_async_index_on_security_findings_unique_columns.rb
new file mode 100644
index 00000000000..e21d3db6798
--- /dev/null
+++ b/db/post_migrate/20220714122418_add_async_index_on_security_findings_unique_columns.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddAsyncIndexOnSecurityFindingsUniqueColumns < Gitlab::Database::Migration[2.0]
+ INDEX_NAME = 'index_security_findings_on_unique_columns'
+
+ disable_ddl_transaction!
+
+ def up
+ prepare_async_index :security_findings, [:uuid, :scan_id, :partition_number], unique: true, name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index_by_name :security_findings, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220715054506_add_parent_link_unique_work_item_index.rb b/db/post_migrate/20220715054506_add_parent_link_unique_work_item_index.rb
new file mode 100644
index 00000000000..ed5b85b711c
--- /dev/null
+++ b/db/post_migrate/20220715054506_add_parent_link_unique_work_item_index.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddParentLinkUniqueWorkItemIndex < Gitlab::Database::Migration[2.0]
+ INDEX_NAME = 'index_work_item_parent_links_on_work_item_id'
+ OLD_INDEX_NAME = 'index_parent_links_on_work_item_id_and_work_item_parent_id'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :work_item_parent_links, :work_item_id, unique: true, name: INDEX_NAME
+ remove_concurrent_index_by_name :work_item_parent_links, OLD_INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index :work_item_parent_links, [:work_item_id, :work_item_parent_id],
+ unique: true, name: OLD_INDEX_NAME
+ remove_concurrent_index_by_name :work_item_parent_links, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20220703182234 b/db/schema_migrations/20220703182234
new file mode 100644
index 00000000000..7d977a4ea9f
--- /dev/null
+++ b/db/schema_migrations/20220703182234
@@ -0,0 +1 @@
+cb1457d19b058add7a966690f8d83e4f7e1612f2de3d6d8a87873bb7fb19960b \ No newline at end of file
diff --git a/db/schema_migrations/20220703182254 b/db/schema_migrations/20220703182254
new file mode 100644
index 00000000000..9d454d07600
--- /dev/null
+++ b/db/schema_migrations/20220703182254
@@ -0,0 +1 @@
+aa4e72f0f6596a609a7620c32e2a5def1ce3ee7200cf7513dd3c6569f68db342 \ No newline at end of file
diff --git a/db/schema_migrations/20220703182314 b/db/schema_migrations/20220703182314
new file mode 100644
index 00000000000..94d06e2075e
--- /dev/null
+++ b/db/schema_migrations/20220703182314
@@ -0,0 +1 @@
+e52d274075c18c3b80ed8306138eabd6dd3d1157dd1093f7f769b0a6cfb56791 \ No newline at end of file
diff --git a/db/schema_migrations/20220706122719 b/db/schema_migrations/20220706122719
new file mode 100644
index 00000000000..6341be22a00
--- /dev/null
+++ b/db/schema_migrations/20220706122719
@@ -0,0 +1 @@
+b80d15b0176f0372a1553920ba72c43a2f9831f786358397f820a83b1b840cdc \ No newline at end of file
diff --git a/db/schema_migrations/20220714122311 b/db/schema_migrations/20220714122311
new file mode 100644
index 00000000000..bb7bb022791
--- /dev/null
+++ b/db/schema_migrations/20220714122311
@@ -0,0 +1 @@
+6e59a39a5d843b5df3b33edb54c51f08062bff7ab1676b9326bb5aa8da159027 \ No newline at end of file
diff --git a/db/schema_migrations/20220714122418 b/db/schema_migrations/20220714122418
new file mode 100644
index 00000000000..a7eeeba0255
--- /dev/null
+++ b/db/schema_migrations/20220714122418
@@ -0,0 +1 @@
+efdfa1c6ffb1b5e4de42bbfd87820eb5d1b87883c8b93cb4cb4101ba928f56dd \ No newline at end of file
diff --git a/db/schema_migrations/20220715054506 b/db/schema_migrations/20220715054506
new file mode 100644
index 00000000000..ad01657f92a
--- /dev/null
+++ b/db/schema_migrations/20220715054506
@@ -0,0 +1 @@
+ecd71a6f9c90bd19a28edcd054ce2ef826859e051dd44c9fea875a5c32040a12 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 299a022de9a..9a3459da7db 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20641,8 +20641,10 @@ CREATE TABLE security_findings (
deduplicated boolean DEFAULT false NOT NULL,
uuid uuid,
overridden_uuid uuid,
+ partition_number integer DEFAULT 1 NOT NULL,
CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)),
- CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40))
+ CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)),
+ CONSTRAINT check_partition_number CHECK ((partition_number = 1))
);
CREATE SEQUENCE security_findings_id_seq
@@ -20710,7 +20712,8 @@ CREATE TABLE security_scans (
project_id bigint,
pipeline_id bigint,
latest boolean DEFAULT true NOT NULL,
- status smallint DEFAULT 0 NOT NULL
+ status smallint DEFAULT 0 NOT NULL,
+ findings_partition_number integer DEFAULT 1 NOT NULL
);
CREATE SEQUENCE security_scans_id_seq
@@ -28985,6 +28988,8 @@ CREATE INDEX index_on_projects_path ON projects USING btree (path);
CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text));
+CREATE INDEX index_on_security_findings_uuid_and_id_order_desc ON security_findings USING btree (uuid, id DESC);
+
CREATE INDEX index_on_users_lower_email ON users USING btree (lower((email)::text));
CREATE INDEX index_on_users_lower_username ON users USING btree (lower((username)::text));
@@ -29153,8 +29158,6 @@ CREATE INDEX index_pages_domains_on_verified_at_and_enabled_until ON pages_domai
CREATE INDEX index_pages_domains_on_wildcard ON pages_domains USING btree (wildcard);
-CREATE UNIQUE INDEX index_parent_links_on_work_item_id_and_work_item_parent_id ON work_item_parent_links USING btree (work_item_id, work_item_parent_id);
-
CREATE INDEX index_partial_ci_builds_on_user_id_name_parser_features ON ci_builds USING btree (user_id, name) WHERE (((type)::text = 'Ci::Build'::text) AND ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('license_scanning'::character varying)::text, ('sast'::character varying)::text, ('coverage_fuzzing'::character varying)::text, ('secret_detection'::character varying)::text])));
CREATE INDEX index_pat_on_user_id_and_expires_at ON personal_access_tokens USING btree (user_id, expires_at);
@@ -30173,6 +30176,8 @@ CREATE UNIQUE INDEX index_wiki_page_slugs_on_slug_and_wiki_page_meta_id ON wiki_
CREATE INDEX index_wiki_page_slugs_on_wiki_page_meta_id ON wiki_page_slugs USING btree (wiki_page_meta_id);
+CREATE UNIQUE INDEX index_work_item_parent_links_on_work_item_id ON work_item_parent_links USING btree (work_item_id);
+
CREATE INDEX index_work_item_parent_links_on_work_item_parent_id ON work_item_parent_links USING btree (work_item_parent_id);
CREATE INDEX index_x509_certificates_on_subject_key_identifier ON x509_certificates USING btree (subject_key_identifier);
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index b0884395556..019032c8b98 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -269,6 +269,17 @@ token is generated when the event destination is created and cannot be changed.
Each streamed event contains a random alphanumeric identifier for the `X-Gitlab-Event-Streaming-Token` HTTP header that can be verified against
the destination's value when [listing streaming destinations](#list-streaming-destinations).
+### Use the GitLab UI
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/360814) in GitLab 15.2.
+
+Users with at least the Owner role for a group can list event streaming destinations and see the verification tokens:
+
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Security & Compliance > Audit events**.
+1. On the main area, select **Streams**.
+1. View the verification token on the right side of each item.
+
## Audit event streaming on Git operations
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.9 [with a flag](../administration/feature_flags.md) named `audit_event_streaming_git_operations`. Disabled by default.
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index e953aafabcf..43f20bfa9ea 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -21,7 +21,7 @@ For public and internal projects, you can change who can see your:
- Pipelines
- Job output logs
- Job artifacts
-- [Pipeline security dashboard](../../user/application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline)
+- [Pipeline security dashboard](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline)
To change the visibility of your pipelines and related features:
diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md
index 993cb33d52d..379a4b3224a 100644
--- a/doc/ci/yaml/artifacts_reports.md
+++ b/doc/ci/yaml/artifacts_reports.md
@@ -52,7 +52,7 @@ GitLab can display the results of one or more reports in:
- The merge request [security widget](../../user/application_security/api_fuzzing/index.md#view-details-of-an-api-fuzzing-vulnerability).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
-- The pipeline [**Security** tab](../../user/application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline).
+- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
- The [security dashboard](../../user/application_security/api_fuzzing/index.md#security-dashboard).
## `artifacts:reports:browser_performance` **(PREMIUM)**
@@ -142,7 +142,7 @@ The collected Container Scanning report uploads to GitLab as an artifact.
GitLab can display the results of one or more reports in:
- The merge request [container scanning widget](../../user/application_security/container_scanning/index.md).
-- The pipeline [**Security** tab](../../user/application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline).
+- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
@@ -156,7 +156,7 @@ The collected coverage fuzzing report uploads to GitLab as an artifact.
GitLab can display the results of one or more reports in:
- The merge request [coverage fuzzing widget](../../user/application_security/coverage_fuzzing/index.md#interacting-with-the-vulnerabilities).
-- The pipeline [**Security** tab](../../user/application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline).
+- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
@@ -168,7 +168,7 @@ report uploads to GitLab as an artifact.
GitLab can display the results of one or more reports in:
- The merge request [security widget](../../user/application_security/dast/index.md#view-details-of-a-vulnerability-detected-by-dast).
-- The pipeline [**Security** tab](../../user/application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline).
+- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
@@ -180,7 +180,7 @@ The collected Dependency Scanning report uploads to GitLab as an artifact.
GitLab can display the results of one or more reports in:
- The merge request [dependency scanning widget](../../user/application_security/dependency_scanning/index.md).
-- The pipeline [**Security** tab](../../user/application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline).
+- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
- The [dependency list](../../user/application_security/dependency_list/).
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index af2566934d1..7777afeed39 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -514,7 +514,7 @@ Not all vulnerabilities have CVEs, and a CVE can be identified multiple times. A
isn't a stable identifier and you shouldn't assume it as such when tracking vulnerabilities.
The maximum number of identifiers for a vulnerability is set as 20. If a vulnerability has more than 20 identifiers,
-the system saves only the first 20 of them. Note that vulnerabilities in the [Pipeline Security](../../user/application_security/security_dashboard/#view-vulnerabilities-in-a-pipeline)
+the system saves only the first 20 of them. Note that vulnerabilities in the [Pipeline Security](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline)
tab do not enforce this limit and all identifiers present in the report artifact are displayed.
#### Details
diff --git a/doc/operations/incident_management/img/timeline_events_v15_1.png b/doc/operations/incident_management/img/timeline_events_v15_1.png
new file mode 100644
index 00000000000..3241f35764c
--- /dev/null
+++ b/doc/operations/incident_management/img/timeline_events_v15_1.png
Binary files differ
diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md
index a6324595312..f1628cb64ca 100644
--- a/doc/operations/incident_management/incidents.md
+++ b/doc/operations/incident_management/incidents.md
@@ -203,6 +203,67 @@ field populated.
![Incident alert details](img/incident_alert_details_v13_4.png)
+### Timeline events
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344059) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `incident_timeline`. Enabled on GitLab.com. Disabled on self-managed.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `incident_timeline`.
+On GitLab.com, this feature is available.
+
+Incident timelines are an important part of record keeping for incidents.
+They give a high-level overview, to executives and external viewers, of what happened during the incident,
+and the steps that were taken for it to be resolved.
+
+#### View the event timeline
+
+Incident timeline events are listed in ascending order of the date and time.
+They are grouped with dates and are listed in ascending order of the time when they occured:
+
+![Incident timeline events list](img/timeline_events_v15_1.png)
+
+To view the event timeline of an incident:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Monitor > Incidents**.
+1. Select an incident.
+1. Select the **Timeline** tab.
+
+#### Create a timeline event
+
+Create a timeline event manually using the form.
+
+Prerequisites:
+
+- You must have at least the Developer role for the project.
+
+To create a timeline event:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Monitor > Incidents**.
+1. Select an incident.
+1. Select the **Timeline** tab.
+1. Select **Add new timeline event**.
+1. Complete the required fields.
+1. Select **Save** or **Save and add another event**.
+
+#### Delete a timeline event
+
+You can also delete timeline events.
+
+Prerequisites:
+
+- You must have at least the Developer role for the project.
+
+To delete a timeline event:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Monitor > Incidents**.
+1. Select an incident.
+1. Select the **Timeline** tab.
+1. On the right of a timeline event, select **More actions** (**{ellipsis_v}**) and then select **Delete**.
+1. To confirm, select **Delete Event**.
+
### Recent updates view **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227836) in GitLab 13.5.
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index 4f03063e0e0..5907860f5cc 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -5,7 +5,7 @@ group: Authentication and Authorization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Enforce two-factor authentication **(FREE SELF)**
+# Enforce two-factor authentication **(FREE)**
Two-factor authentication (2FA) provides an additional level of security to your
users' GitLab account. When enabled, users are prompted for a code generated by an application in
@@ -13,7 +13,7 @@ addition to supplying their username and password to sign in.
Read more about [two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md)
-## Enforce 2FA for all users
+## Enforce 2FA for all users **(FREE SELF)**
Users on GitLab can enable it without any administrator's intervention. If you
want to enforce everyone to set up 2FA, you can choose from two different ways:
@@ -33,7 +33,7 @@ To enable 2FA for all users:
If you want 2FA enforcement to take effect during the next sign-in attempt,
change the grace period to `0`.
-## Disable 2FA enforcement through Rails console
+### Disable 2FA enforcement through Rails console
Using the [Rails console](../administration/operations/rails_console.md), enforcing 2FA for
all user can be disabled. Connect to the Rails console and run:
@@ -86,7 +86,7 @@ The following are important notes about 2FA:
- Access tokens are not required to provide a second factor for authentication because they are API-based.
Tokens generated before 2FA is enforced remain valid.
-## Disable 2FA
+## Disable 2FA **(FREE SELF)**
WARNING:
Disabling 2FA for users does not disable the [enforce 2FA for all users](#enforce-2fa-for-all-users)
diff --git a/doc/update/index.md b/doc/update/index.md
index ac395fa1af2..0b8b76aaa33 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -492,6 +492,7 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
to avoid the database crashing.
- The use of encrypted S3 buckets with storage-specific configuration is no longer supported after [removing support for using `background_upload`](removals.md#background-upload-for-object-storage).
- The [certificate-based Kubernetes integration (DEPRECATED)](../user/infrastructure/clusters/index.md#certificate-based-kubernetes-integration-deprecated) is disabled by default, but you can be re-enable it through the [`certificate_based_clusters` feature flag](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) until GitLab 16.0.
+- When you use the GitLab Helm Chart project with a custom `serviceAccount`, ensure it has `get` and `list` permissions for the `serviceAccount` and `secret` resources.
### 14.10.0
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 218799ac217..be98cc36174 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -224,7 +224,7 @@ From the merge request security widget, select **Expand** to unfold the widget,
A pipeline's security tab lists all findings in the current branch. It includes new findings introduced by this branch and existing vulnerabilities that were already present when the branch was created. These results likely do not match the findings displayed in the Merge Request security widget as those do not include the existing vulnerabilities (with the exception of showing any existing vulnerabilities that are no longer detected in the feature branch).
-For more details, see [security tab](security_dashboard/index.md#view-vulnerabilities-in-a-pipeline).
+For more details, see [security tab](vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
## View security scan information in the Security Dashboard
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 3cb4bd4a02d..f3c834e06c7 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -42,57 +42,6 @@ To reduce false negatives in [dependency scans](../../../user/application_securi
- Python projects can have lock files, but GitLab Secure tools don't support them.
- Configure your project for [Continuous Delivery](../../../ci/introduction/index.md).
-## View vulnerabilities in a pipeline
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13496) in GitLab 12.3.
-
-To view vulnerabilities in a pipeline:
-
-1. On the top bar, select **Menu > Projects** and find your project.
-1. On the left sidebar, select **CI/CD > Pipelines**.
-1. From the list, select the pipeline you want to check for vulnerabilities.
-1. Select the **Security** tab.
-
-**Scan details** shows vulnerabilities introduced by the merge request, in addition to existing vulnerabilities
-from the latest successful pipeline in your project's default branch.
-
-A pipeline consists of multiple jobs, such as SAST and DAST scans. If a job fails to finish,
-the security dashboard doesn't show SAST scanner output. For example, if the SAST
-job finishes but the DAST job fails, the security dashboard doesn't show SAST results. On failure,
-the analyzer outputs an [exit code](../../../development/integrations/secure.md#exit-code).
-
-## View total number of vulnerabilities per scan
-
-To view the total number of vulnerabilities per scan:
-
-1. On the top bar, select **Menu > Projects** and find your project.
-1. On the left sidebar, select **CI/CD > Pipelines**.
-1. Select the **Status** of a branch.
-1. Select the **Security** tab.
-
-**Scan details** shows vulnerabilities introduced by the merge request, in addition to existing vulnerabilities
-from the latest successful pipeline in your project's default branch.
-
-### Download security scan outputs
-
-> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3728) in GitLab 13.10.
-> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/333660) in GitLab 14.2.
-
-Depending on the type of security scanner, you can download:
-
-- A JSON artifact that contains the security scanner [report](../../../development/integrations/secure.md#report).
-- A CSV file that contains URLs and endpoints scanned by the security scanner.
-
-To download a security scan output:
-
-1. On the top bar, select **Menu > Projects** and find your project.
-1. On the left sidebar, select **CI/CD > Pipelines**.
-1. Select the **Status** of a branch.
-1. Select the **Security** tab.
-1. In **Scan details**, select **Download results**:
- - To download a JSON file, select the JSON artifact.
- - To download a CSV file, select **Download scanned resources**.
-
## View vulnerabilities over time for a project
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235558) in GitLab 13.6.
diff --git a/doc/user/application_security/vulnerability_report/pipeline.md b/doc/user/application_security/vulnerability_report/pipeline.md
new file mode 100644
index 00000000000..980bd99f8c8
--- /dev/null
+++ b/doc/user/application_security/vulnerability_report/pipeline.md
@@ -0,0 +1,57 @@
+---
+type: reference, howto
+stage: Secure
+group: Threat Insights
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# View vulnerabilities in a pipeline
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13496) in GitLab 12.3.
+
+To view vulnerabilities in a pipeline:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **CI/CD > Pipelines**.
+1. From the list, select the pipeline you want to check for vulnerabilities.
+1. Select the **Security** tab.
+
+**Scan details** shows vulnerabilities introduced by the merge request, in addition to existing vulnerabilities
+from the latest successful pipeline in your project's default branch.
+
+A pipeline consists of multiple jobs, such as SAST and DAST scans. If a job fails to finish,
+the security dashboard doesn't show SAST scanner output. For example, if the SAST
+job finishes but the DAST job fails, the security dashboard doesn't show SAST results. On failure,
+the analyzer outputs an [exit code](../../../development/integrations/secure.md#exit-code).
+
+## View total number of vulnerabilities per scan
+
+To view the total number of vulnerabilities per scan:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **CI/CD > Pipelines**.
+1. Select the **Status** of a branch.
+1. Select the **Security** tab.
+
+**Scan details** shows vulnerabilities introduced by the merge request, in addition to existing vulnerabilities
+from the latest successful pipeline in your project's default branch.
+
+## Download security scan outputs
+
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3728) in GitLab 13.10.
+> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/333660) in GitLab 14.2.
+
+Depending on the type of security scanner, you can download:
+
+- A JSON artifact that contains the security scanner [report](../../../development/integrations/secure.md#report).
+- A CSV file that contains URLs and endpoints scanned by the security scanner.
+
+To download a security scan output:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **CI/CD > Pipelines**.
+1. Select the **Status** of a branch.
+1. Select the **Security** tab.
+1. In **Scan details**, select **Download results**:
+ - To download a JSON file, select the JSON artifact.
+ - To download a CSV file, select **Download scanned resources**.
diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md
index 4130b2f6221..9282ac7fb40 100644
--- a/doc/user/clusters/agent/install/index.md
+++ b/doc/user/clusters/agent/install/index.md
@@ -89,8 +89,12 @@ You must register an agent before you can install the agent in your cluster. To
- If you want to create a configuration with CI/CD defaults, type a name.
- If you already have an [agent configuration file](#create-an-agent-configuration-file), select it from the list.
1. Select **Register an agent**.
-1. GitLab generates an access token for the agent. Securely store this token. You need it to install the agent
+1. GitLab generates an access token for the agent. You need this token to install the agent
in your cluster and to [update the agent](#update-the-agent-version) to another version.
+
+ WARNING:
+ Securely store the agent access token. A bad actor can use this token to access source code in the agent's configuration project, access source code in any public project on the GitLab instance, or even, under very specific conditions, obtain a Kubernetes manifest.
+
1. Copy the command under **Recommended installation method**. You need it when you use
the one-liner installation method to install the agent in your cluster.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 91df2a03863..b01bfbef3aa 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -264,7 +264,7 @@ More details about the permissions for some project-level features follow.
| View pipeline details page | ✓ (*1*) | ✓ (*2*) | ✓ | ✓ | ✓ | ✓ |
| View pipelines page | ✓ (*1*) | ✓ (*2*) | ✓ | ✓ | ✓ | ✓ |
| View pipelines tab in MR | ✓ (*3*) | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
-| [View vulnerabilities in a pipeline](application_security/security_dashboard/index.md#view-vulnerabilities-in-a-pipeline) | | ✓ (*2*) | ✓ | ✓ | ✓ | ✓ |
+| [View vulnerabilities in a pipeline](application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline) | | ✓ (*2*) | ✓ | ✓ | ✓ | ✓ |
| View and download project-level [Secure Files](../api/secure_files.md) | | | | ✓ | ✓ | ✓ |
| Cancel and retry jobs | | | | ✓ | ✓ | ✓ |
| Create new [environments](../ci/environments/index.md) | | | | ✓ | ✓ | ✓ |
diff --git a/doc/user/project/milestones/burndown_and_burnup_charts.md b/doc/user/project/milestones/burndown_and_burnup_charts.md
index d6fcd9fbb16..298eea8639c 100644
--- a/doc/user/project/milestones/burndown_and_burnup_charts.md
+++ b/doc/user/project/milestones/burndown_and_burnup_charts.md
@@ -66,7 +66,7 @@ A burndown chart is available for every project or group milestone that has been
date** and a **due date**.
NOTE:
-You're able to [promote project](index.md#promoting-project-milestones-to-group-milestones) to group milestones and still see the **burndown chart** for them, respecting license limitations.
+You're able to [promote project](index.md#promote-a-project-milestone-to-a-group-milestone) to group milestones and still see the **burndown chart** for them, respecting license limitations.
The chart indicates the project's progress throughout that milestone (for issues assigned to it).
diff --git a/doc/user/project/milestones/img/milestones_promote_milestone.png b/doc/user/project/milestones/img/milestones_promote_milestone.png
deleted file mode 100644
index 2ef85c5951d..00000000000
--- a/doc/user/project/milestones/img/milestones_promote_milestone.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index b0f3179961d..3ecee86b978 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -23,87 +23,127 @@ Additionally, you can integrate milestones with the [Releases feature](../releas
## Project milestones and group milestones
-You can assign **project milestones** to issues or merge requests in that project only.
-To view the project milestone list, in a project, go to **{issues}** **Issues > Milestones**.
+A milestone can belong to [project](../index.md) or [group](../../group/index.md).
+You can assign **project milestones** to issues or merge requests in that project only.
You can assign **group milestones** to any issue or merge request of any project in that group.
-To view the group milestone list, in a group, go to **{issues}** **Issues > Milestones**.
-
-You can also view all milestones you have access to in the dashboard milestones list.
-To view both project milestones and group milestones you have access to, select **Menu > Milestones**
-on the top bar.
For information about project and group milestones API, see:
- [Project Milestones API](../../../api/milestones.md)
- [Group Milestones API](../../../api/group_milestones.md)
-NOTE:
-If you're in a group and select **Issues > Milestones**, GitLab displays group milestones
-and the milestones of projects in this group.
-If you're in a project and select **Issues > Milestones**, GitLab displays only this project's milestones.
+### View project or group milestones
+
+To view the milestone list:
+
+1. On the top bar, select **Menu > Projects** and find your project or
+ **Menu > Groups** and find your group.
+1. Select **Issues > Milestones**.
+
+In a project, GitLab displays milestones that belong to the project.
+In a group, GitLab displays milestones that belong to the group and all projects in the group.
+
+### View all milestones
-## Creating milestones
+You can view all the milestones you have access to in the entire GitLab namespace.
+You might not see some milestones because they're in projects or groups you're not a member of.
+
+To do so, on the top bar select **Menu > Milestones**.
+
+## Create a milestone
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
-Milestones can be created either at project or group level.
+You can create a milestone either in a project or a group.
Prerequisites:
-- You must have at least the Reporter role for a group.
+- You must have at least the Reporter role for the project or group the milestone belongs to.
To create a milestone:
1. On the top bar, select **Menu > Projects** and find your project or **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Milestones**.
1. Select **New milestone**.
-1. Enter the title, an optional description, an optional start date, and an optional due date.
+1. Enter the title.
+1. Optional. Enter description, start date, and due date.
1. Select **New milestone**.
![New milestone](img/milestones_new_project_milestone.png)
-## Editing milestones
+## Edit a milestone
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
-Users with at least the Reporter role can edit milestones.
-
Prerequisites:
-- You must have at least the Reporter role for a group.
+- You must have at least the Reporter role for the project or group the milestone belongs to.
To edit a milestone:
-1. In a project or group, go to **{issues}** **Issues > Milestones**.
+1. On the top bar, select **Menu > Projects** and find your project or **Menu > Groups** and find your group.
1. Select a milestone's title.
1. Select **Edit**.
+1. Edit the title, start date, due date, or description.
+1. Select **Save changes**.
+
+## Delete a milestone
+
+> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
-You can delete a milestone by selecting the **Delete** button.
+Prerequisites:
+
+- You must have at least the Reporter role for the project or group the milestone belongs to.
+
+To edit a milestone:
+
+1. On the top bar, select **Menu > Projects** and find your project or **Menu > Groups** and find your group.
+1. Select a milestone's title.
+1. Select **Delete**.
+1. Select **Delete milestone**.
-### Promoting project milestones to group milestones
+## Promote a project milestone to a group milestone
If you are expanding the number of projects in a group, you might want to share the same milestones
-among this group's projects. You can also promote project milestones to group milestones in order to
+among this group's projects. You can also promote project milestones to group milestones to
make them available to other projects in the same group.
-From the project milestone list page, you can promote a project milestone to a group milestone.
-This merges all project milestones across all projects in this group with the same name into a single
-group milestones. All issues and merge requests that were previously assigned to one of these project
-milestones is assigned the new group milestones. This action cannot be reversed and the changes are
-permanent.
+Promoting a milestone merges all project milestones across all projects in this group with the same
+name into a single group milestone.
+All issues and merge requests that were previously assigned to one of these project
+milestones become assigned to the new group milestone.
WARNING:
-From GitLab 12.4 and earlier, some information is lost when you promote a project milestone to a
-group milestone. Not all features on the project milestone view are available on the group milestone
-view. If you promote a project milestone to a group milestone, you lose these features. Visit
-[Milestone view](#milestone-view) to learn which features are missing from the group milestone view.
+This action cannot be reversed and the changes are permanent.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the group.
+
+To promote a project milestone:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. Either:
+ - Select **Promote to Group Milestone** (**{level-up}**).
+ - Select the milestone title, and then select **Promote**.
+1. Select **Promote Milestone**.
+
+## Assign a milestone to an issue or merge request
+
+Every issue and merge request can be assigned one milestone.
+The milestones are visible on every issue and merge request page, on the right sidebar.
+They are also visible in the issue board.
-![Promote milestone](img/milestones_promote_milestone.png)
+To assign or unassign a milestone:
-## Assigning milestones from the sidebar
+1. View an issue or a merge request.
+1. On the right sidebar, next to **Milestones**, select **Edit**.
+1. In the **Assign milestone** list, search for a milestone by typing its name.
+ You can select from both project and group milestones.
+1. Select the milestone you want to assign.
-Every issue and merge request can be assigned a milestone. The milestones are visible on every issue and merge request page, in the sidebar. They are also visible in the issue board. From the sidebar, you can assign or unassign a milestones to the object. You can also perform this as a [quick action](../quick_actions.md) in a comment. [As mentioned](#project-milestones-and-group-milestones), for a given issue or merge request, both project milestones and group milestones can be selected and assigned to the object.
+You can also use the `/assign` [quick action](../quick_actions.md) in a comment.
## Filtering issues and merge requests by milestone
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index 306afd85261..36236f2969e 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -49,4 +49,4 @@ To edit a task:
To delete a task:
1. In the issue description, select the task.
-1. From the options menu (**{ellipsis_v}**), select **Delete work item**.
+1. From the options menu (**{ellipsis_v}**), select **Delete task**.
diff --git a/lib/gitlab/ci/runner_upgrade_check.rb b/lib/gitlab/ci/runner_upgrade_check.rb
index 7b437f97d60..10a89bb15d4 100644
--- a/lib/gitlab/ci/runner_upgrade_check.rb
+++ b/lib/gitlab/ci/runner_upgrade_check.rb
@@ -20,9 +20,8 @@ module Gitlab
return { recommended: recommended_version } if recommended_version
# Consider update if there's a newer release within the currently deployed GitLab version
- if available_runner_release(runner_version)
- return { available: runner_releases_store.releases_by_minor[gitlab_version.without_patch] }
- end
+ available_version = available_runner_release(runner_version)
+ return { available: available_version } if available_version
{ not_available: runner_version }
end
@@ -31,18 +30,21 @@ module Gitlab
def recommended_runner_release_update(runner_version)
recommended_release = runner_releases_store.releases_by_minor[runner_version.without_patch]
+ return recommended_release if recommended_release && recommended_release > runner_version
- recommended_release if recommended_release && recommended_release > runner_version
+ # Consider the edge case of pre-release runner versions that get registered, but are never published.
+ # In this case, suggest the latest compatible runner version
+ latest_release = runner_releases_store.releases_by_minor.values.select { |v| v < gitlab_version }.max
+ latest_release if latest_release && latest_release > runner_version
end
def available_runner_release(runner_version)
available_release = runner_releases_store.releases_by_minor[gitlab_version.without_patch]
-
available_release if available_release && available_release > runner_version
end
def gitlab_version
- @gitlab_version ||= ::Gitlab::VersionInfo.parse(::Gitlab::VERSION)
+ @gitlab_version ||= ::Gitlab::VersionInfo.parse(::Gitlab::VERSION, parse_suffix: true)
end
def runner_releases_store
diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb
index 32e653fb6e4..c4b0e24e34a 100644
--- a/lib/gitlab/import_export/project/relation_factory.rb
+++ b/lib/gitlab/import_export/project/relation_factory.rb
@@ -89,6 +89,7 @@ module Gitlab
when :'Ci::PipelineSchedule' then setup_pipeline_schedule
when :'ProtectedBranch::MergeAccessLevel' then setup_protected_branch_access_level
when :'ProtectedBranch::PushAccessLevel' then setup_protected_branch_access_level
+ when :releases then setup_release
end
update_project_references
@@ -150,6 +151,14 @@ module Gitlab
@relation_hash['relative_position'] = compute_relative_position
end
+ def setup_release
+ # When author is not present for source release set the author as ghost user.
+
+ if @relation_hash['author_id'].blank?
+ @relation_hash['author_id'] = User.select(:id).ghost.id
+ end
+ end
+
def setup_pipeline_schedule
@relation_hash['active'] = false
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8254899c809..d49710656e4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5358,6 +5358,9 @@ msgstr ""
msgid "AuditStreams|Value"
msgstr ""
+msgid "AuditStreams|Verification token"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -16215,6 +16218,9 @@ msgstr ""
msgid "February"
msgstr ""
+msgid "Feedback issue"
+msgstr ""
+
msgid "Fetch and check out this merge request's feature branch:"
msgstr ""
@@ -28090,6 +28096,9 @@ msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
+msgid "Performance insights"
+msgstr ""
+
msgid "Performance optimization"
msgstr ""
@@ -28837,9 +28846,18 @@ msgstr ""
msgid "Pipeline|Failed"
msgstr ""
+msgid "Pipeline|Five slowest jobs"
+msgstr ""
+
msgid "Pipeline|In progress"
msgstr ""
+msgid "Pipeline|Last executed job"
+msgstr ""
+
+msgid "Pipeline|Longest queued job"
+msgstr ""
+
msgid "Pipeline|Manual"
msgstr ""
@@ -28852,12 +28870,18 @@ msgstr ""
msgid "Pipeline|Merged result pipeline"
msgstr ""
+msgid "Pipeline|Only able to show first 100 results"
+msgstr ""
+
msgid "Pipeline|Passed"
msgstr ""
msgid "Pipeline|Pending"
msgstr ""
+msgid "Pipeline|Performance insights"
+msgstr ""
+
msgid "Pipeline|Pipeline"
msgstr ""
@@ -28912,6 +28936,12 @@ msgstr ""
msgid "Pipeline|Test coverage"
msgstr ""
+msgid "Pipeline|The last executed job is the last job to start in the pipeline."
+msgstr ""
+
+msgid "Pipeline|The longest queued job is the job that spent the longest time in the pending state, waiting to be picked up by a Runner"
+msgstr ""
+
msgid "Pipeline|This change will decrease the overall test coverage if merged."
msgstr ""
@@ -28939,6 +28969,9 @@ msgstr ""
msgid "Pipeline|View commit"
msgstr ""
+msgid "Pipeline|View dependency"
+msgstr ""
+
msgid "Pipeline|View pipeline"
msgstr ""
@@ -43870,7 +43903,7 @@ msgstr ""
msgid "WorkItem|Are you sure you want to cancel editing?"
msgstr ""
-msgid "WorkItem|Are you sure you want to delete the work item? This action cannot be reversed."
+msgid "WorkItem|Are you sure you want to delete the task? This action cannot be reversed."
msgstr ""
msgid "WorkItem|Assignee"
@@ -43896,7 +43929,7 @@ msgstr ""
msgid "WorkItem|Create work item"
msgstr ""
-msgid "WorkItem|Delete work item"
+msgid "WorkItem|Delete task"
msgstr ""
msgid "WorkItem|Expand child items"
@@ -43917,9 +43950,15 @@ msgstr ""
msgid "WorkItem|Select type"
msgstr ""
+msgid "WorkItem|Something went wrong when creating a task. Please try again"
+msgstr ""
+
msgid "WorkItem|Something went wrong when creating a work item. Please try again"
msgstr ""
+msgid "WorkItem|Something went wrong when deleting the task. Please try again."
+msgstr ""
+
msgid "WorkItem|Something went wrong when deleting the work item. Please try again."
msgstr ""
@@ -43935,6 +43974,9 @@ msgstr ""
msgid "WorkItem|Something went wrong while updating the work item. Please try again."
msgstr ""
+msgid "WorkItem|Task deleted"
+msgstr ""
+
msgid "WorkItem|Type"
msgstr ""
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index e0b61526ba0..0f6845dc5ee 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -7,7 +7,7 @@ FactoryBot.define do
project
trait :token do
- token { SecureRandom.hex(10) }
+ token { generate(:token) }
end
trait :all_events_enabled do
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index c10fab8588d..fd7f9223965 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -22,4 +22,5 @@ FactoryBot.define do
sequence(:job_name) { |n| "job #{n}" }
sequence(:work_item_type_name) { |n| "bug#{n}" }
sequence(:short_text) { |n| "someText#{n}" }
+ sequence(:token) { SecureRandom.hex(10) }
end
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index e721525f00c..12dbabf833b 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -2361,14 +2361,53 @@
"releases": [
{
"id": 1,
+ "tag": "release-1.0",
+ "description": "Some release notes",
+ "project_id": 5,
+ "created_at": "2019-12-25T10:17:14.621Z",
+ "updated_at": "2019-12-25T10:17:14.621Z",
+ "author_id": null,
+ "name": "release-1.0",
+ "sha": "902de3a8bd5573f4a049b1457d28bc1592baaa2e",
+ "released_at": "2019-12-25T10:17:14.615Z",
+ "links": [
+ {
+ "id": 1,
+ "release_id": 1,
+ "url": "http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download",
+ "name": "release-1.0.dmg",
+ "created_at": "2019-12-25T10:17:14.621Z",
+ "updated_at": "2019-12-25T10:17:14.621Z"
+ }
+ ],
+ "milestone_releases": [
+ {
+ "milestone_id": 1349,
+ "release_id": 9172,
+ "milestone": {
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1
+ }
+ }
+ ]
+ },
+ {
+ "id": 2,
"tag": "release-1.1",
"description": "Some release notes",
"project_id": 5,
"created_at": "2019-12-26T10:17:14.621Z",
"updated_at": "2019-12-26T10:17:14.621Z",
- "author_id": 1,
+ "author_id": 16,
"name": "release-1.1",
- "sha": "901de3a8bd5573f4a049b1457d28bc1592ba6bf9",
+ "sha": "902de3a8bd5573f4a049b1457d28bc1592ba6bg9",
"released_at": "2019-12-26T10:17:14.615Z",
"links": [
{
@@ -2397,6 +2436,45 @@
}
}
]
+ },
+ {
+ "id": 3,
+ "tag": "release-1.2",
+ "description": "Some release notes",
+ "project_id": 5,
+ "created_at": "2019-12-27T10:17:14.621Z",
+ "updated_at": "2019-12-27T10:17:14.621Z",
+ "author_id": 1,
+ "name": "release-1.2",
+ "sha": "903de3a8bd5573f4a049b1457d28bc1592ba6bf9",
+ "released_at": "2019-12-27T10:17:14.615Z",
+ "links": [
+ {
+ "id": 1,
+ "release_id": 1,
+ "url": "http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download",
+ "name": "release-1.2.dmg",
+ "created_at": "2019-12-27T10:17:14.621Z",
+ "updated_at": "2019-12-27T10:17:14.621Z"
+ }
+ ],
+ "milestone_releases": [
+ {
+ "milestone_id": 1349,
+ "release_id": 9172,
+ "milestone": {
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1
+ }
+ }
+ ]
}
],
"project_members": [
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson
index a194898cb5a..dfbde1f2598 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson
@@ -1 +1,3 @@
-{"id":1,"tag":"release-1.1","description":"Some release notes","project_id":5,"created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z","author_id":1,"name":"release-1.1","sha":"901de3a8bd5573f4a049b1457d28bc1592ba6bf9","released_at":"2019-12-26T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.1.dmg","created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]}
+{"id":1,"tag":"release-1.0","description":"Some release notes","project_id":5,"created_at":"2019-12-25T10:17:14.621Z","updated_at":"2019-12-25T10:17:14.621Z","author_id":null,"name":"release-1.0","sha":"901de3a8bd5573f4a049b1457d28bc1592baaa2e","released_at":"2019-12-25T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.0.dmg","created_at":"2019-12-25T10:17:14.621Z","updated_at":"2019-12-25T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]}
+{"id":2,"tag":"release-1.1","description":"Some release notes","project_id":5,"created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z","author_id":16,"name":"release-1.1","sha":"902de3a8bd5573f4a049b1457d28bc1592ba6bg9","released_at":"2019-12-26T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.1.dmg","created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]}
+{"id":3,"tag":"release-1.2","description":"Some release notes","project_id":5,"created_at":"2019-12-27T10:17:14.621Z","updated_at":"2019-12-27T10:17:14.621Z","author_id":1,"name":"release-1.2","sha":"903de3a8bd5573f4a049b1457d28bc1592ba6bf9","released_at":"2019-12-27T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.2.dmg","created_at":"2019-12-27T10:17:14.621Z","updated_at":"2019-12-27T10:17:14.621Z"}],"milestone_releases":[{"milestone_id":1349,"release_id":9172,"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1}}]}
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index b8a2da4fa47..8ee57f97754 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -332,7 +332,7 @@ describe('Description component', () => {
findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
- expect($toast.show).toHaveBeenCalledWith('Work item deleted');
+ expect($toast.show).toHaveBeenCalledWith('Task deleted');
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 34784a88ba9..3eaf06e0656 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -30,10 +30,16 @@ import * as Api from '~/pipelines/components/graph_shared/api';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
+import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
-import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
+import {
+ mapCallouts,
+ mockCalloutsResponse,
+ mockPipelineResponse,
+ mockPerformanceInsightsResponse,
+} from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@@ -89,11 +95,15 @@ describe('Pipeline graph wrapper', () => {
const callouts = mapCallouts(calloutsList);
const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts));
const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData);
+ const getPerformanceInsightsHandler = jest
+ .fn()
+ .mockResolvedValue(mockPerformanceInsightsResponse);
const requestHandlers = [
[getPipelineHeaderData, getPipelineHeaderDataHandler],
[getPipelineDetails, getPipelineDetailsHandler],
[getUserCallouts, getUserCalloutsHandler],
+ [getPerformanceInsights, getPerformanceInsightsHandler],
];
const apolloProvider = createMockApollo(requestHandlers);
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 4e79c7e73cc..1397500bdc7 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -1,10 +1,19 @@
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
+import { mockPerformanceInsightsResponse } from './mock_data';
+
+Vue.use(VueApollo);
describe('the graph view selector component', () => {
let wrapper;
+ let trackingSpy;
const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
const findViewTypeSelector = () => wrapper.findComponent(GlButtonGroup);
@@ -13,11 +22,13 @@ describe('the graph view selector component', () => {
const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]');
const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon);
const findHoverTip = () => wrapper.findComponent(GlAlert);
+ const findPipelineInsightsBtn = () => wrapper.find('[data-testid="pipeline-insights-btn"]');
const defaultProps = {
showLinks: false,
tipPreviouslyDismissed: false,
type: STAGE_VIEW,
+ isPipelineComplete: true,
};
const defaultData = {
@@ -27,6 +38,14 @@ describe('the graph view selector component', () => {
showLinksActive: false,
};
+ const getPerformanceInsightsHandler = jest
+ .fn()
+ .mockResolvedValue(mockPerformanceInsightsResponse);
+
+ const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]];
+
+ const apolloProvider = createMockApollo(requestHandlers);
+
const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => {
wrapper = mountFn(GraphViewSelector, {
propsData: {
@@ -39,6 +58,7 @@ describe('the graph view selector component', () => {
...data,
};
},
+ apolloProvider,
});
};
@@ -202,5 +222,44 @@ describe('the graph view selector component', () => {
expect(findHoverTip().exists()).toBe(false);
});
});
+
+ describe('pipeline insights', () => {
+ it.each`
+ isPipelineComplete | shouldShow
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'button should display $shouldShow if isPipelineComplete is $isPipelineComplete ',
+ ({ isPipelineComplete, shouldShow }) => {
+ createComponent({
+ props: {
+ isPipelineComplete,
+ },
+ });
+
+ expect(findPipelineInsightsBtn().exists()).toBe(shouldShow);
+ },
+ );
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks performance insights button click', () => {
+ findPipelineInsightsBtn().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_insights_button', {
+ label: 'performance_insights',
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 6124d67af09..959bbcefc98 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1038,3 +1038,245 @@ export const triggerJob = {
action: null,
},
};
+
+export const mockPerformanceInsightsResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ __typename: 'Pipeline',
+ id: 'gid://gitlab/Ci::Pipeline/97',
+ jobs: {
+ __typename: 'CiJobConnection',
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ },
+ nodes: [
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Bridge/2502',
+ duration: null,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2502-2502',
+ detailsPath: '/root/lots-of-jobs-project/-/pipelines/98',
+ },
+ name: 'trigger_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/303',
+ name: 'deploy',
+ },
+ startedAt: null,
+ queuedDuration: 424850.376278,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2501',
+ duration: 10,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2501-2501',
+ detailsPath: '/root/ci-project/-/jobs/2501',
+ },
+ name: 'artifact_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/303',
+ name: 'deploy',
+ },
+ startedAt: '2022-07-01T16:31:41Z',
+ queuedDuration: 2.621553,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2500',
+ duration: 4,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2500-2500',
+ detailsPath: '/root/ci-project/-/jobs/2500',
+ },
+ name: 'coverage_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/302',
+ name: 'test',
+ },
+ startedAt: '2022-07-01T16:31:33Z',
+ queuedDuration: 14.388869,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2499',
+ duration: 4,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2499-2499',
+ detailsPath: '/root/ci-project/-/jobs/2499',
+ },
+ name: 'test_job_two',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/302',
+ name: 'test',
+ },
+ startedAt: '2022-07-01T16:31:28Z',
+ queuedDuration: 15.792664,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2498',
+ duration: 4,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2498-2498',
+ detailsPath: '/root/ci-project/-/jobs/2498',
+ },
+ name: 'test_job_one',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/302',
+ name: 'test',
+ },
+ startedAt: '2022-07-01T16:31:17Z',
+ queuedDuration: 8.317072,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2497',
+ duration: 5,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'failed-2497-2497',
+ detailsPath: '/root/ci-project/-/jobs/2497',
+ },
+ name: 'allow_failure_test_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/302',
+ name: 'test',
+ },
+ startedAt: '2022-07-01T16:31:22Z',
+ queuedDuration: 3.547553,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2496',
+ duration: null,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'manual-2496-2496',
+ detailsPath: '/root/ci-project/-/jobs/2496',
+ },
+ name: 'test_manual_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/302',
+ name: 'test',
+ },
+ startedAt: null,
+ queuedDuration: null,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2495',
+ duration: 5,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2495-2495',
+ detailsPath: '/root/ci-project/-/jobs/2495',
+ },
+ name: 'large_log_output',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/301',
+ name: 'build',
+ },
+ startedAt: '2022-07-01T16:31:11Z',
+ queuedDuration: 79.128625,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2494',
+ duration: 5,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2494-2494',
+ detailsPath: '/root/ci-project/-/jobs/2494',
+ },
+ name: 'build_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/301',
+ name: 'build',
+ },
+ startedAt: '2022-07-01T16:31:05Z',
+ queuedDuration: 73.286895,
+ },
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Build/2493',
+ duration: 16,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2493-2493',
+ detailsPath: '/root/ci-project/-/jobs/2493',
+ },
+ name: 'wait_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/301',
+ name: 'build',
+ },
+ startedAt: '2022-07-01T16:30:48Z',
+ queuedDuration: 56.258856,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const mockPerformanceInsightsNextPageResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ __typename: 'Pipeline',
+ id: 'gid://gitlab/Ci::Pipeline/97',
+ jobs: {
+ __typename: 'CiJobConnection',
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ },
+ nodes: [
+ {
+ __typename: 'CiJob',
+ id: 'gid://gitlab/Ci::Bridge/2502',
+ duration: null,
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-2502-2502',
+ detailsPath: '/root/lots-of-jobs-project/-/pipelines/98',
+ },
+ name: 'trigger_job',
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/303',
+ name: 'deploy',
+ },
+ startedAt: null,
+ queuedDuration: 424850.376278,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/pipelines/performance_insights_modal_spec.js b/spec/frontend/pipelines/performance_insights_modal_spec.js
new file mode 100644
index 00000000000..b745eb1d78e
--- /dev/null
+++ b/spec/frontend/pipelines/performance_insights_modal_spec.js
@@ -0,0 +1,122 @@
+import { GlAlert, GlLink, GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import PerformanceInsightsModal from '~/pipelines/components/performance_insights_modal.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { trimText } from 'helpers/text_helper';
+import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql';
+import {
+ mockPerformanceInsightsResponse,
+ mockPerformanceInsightsNextPageResponse,
+} from './graph/mock_data';
+
+Vue.use(VueApollo);
+
+describe('Performance insights modal', () => {
+ let wrapper;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data');
+ const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link');
+ const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data');
+ const findExecutedCardLink = () => wrapper.findByTestId('insights-executed-card-link');
+ const findSlowJobsStage = (index) => wrapper.findAllByTestId('insights-slow-job-stage').at(index);
+ const findSlowJobsLink = (index) => wrapper.findAllByTestId('insights-slow-job-link').at(index);
+
+ const getPerformanceInsightsHandler = jest
+ .fn()
+ .mockResolvedValue(mockPerformanceInsightsResponse);
+
+ const getPerformanceInsightsNextPageHandler = jest
+ .fn()
+ .mockResolvedValue(mockPerformanceInsightsNextPageResponse);
+
+ const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]];
+
+ const createComponent = (handlers = requestHandlers) => {
+ wrapper = shallowMountExtended(PerformanceInsightsModal, {
+ provide: {
+ pipelineIid: '1',
+ pipelineProjectPath: 'root/ci-project',
+ },
+ apolloProvider: createMockApollo(handlers),
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('without next page', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('does not dispaly alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ describe('queued duration card', () => {
+ it('displays card data', () => {
+ expect(trimText(findQueuedCardData().text())).toBe('4.9 days');
+ });
+ it('displays card link', () => {
+ expect(findQueuedCardLink().attributes('href')).toBe(
+ '/root/lots-of-jobs-project/-/pipelines/98',
+ );
+ });
+ });
+
+ describe('executed duration card', () => {
+ it('displays card data', () => {
+ expect(trimText(findExecutedCardData().text())).toBe('trigger_job');
+ });
+ it('displays card link', () => {
+ expect(findExecutedCardLink().attributes('href')).toBe(
+ '/root/lots-of-jobs-project/-/pipelines/98',
+ );
+ });
+ });
+
+ describe('slow jobs', () => {
+ it.each`
+ index | expectedStage | expectedName | expectedLink
+ ${0} | ${'build'} | ${'wait_job'} | ${'/root/ci-project/-/jobs/2493'}
+ ${1} | ${'deploy'} | ${'artifact_job'} | ${'/root/ci-project/-/jobs/2501'}
+ ${2} | ${'test'} | ${'allow_failure_test_job'} | ${'/root/ci-project/-/jobs/2497'}
+ ${3} | ${'build'} | ${'large_log_output'} | ${'/root/ci-project/-/jobs/2495'}
+ ${4} | ${'build'} | ${'build_job'} | ${'/root/ci-project/-/jobs/2494'}
+ `(
+ 'should display slow job correctly',
+ ({ index, expectedStage, expectedName, expectedLink }) => {
+ expect(findSlowJobsStage(index).text()).toBe(expectedStage);
+ expect(findSlowJobsLink(index).text()).toBe(expectedName);
+ expect(findSlowJobsLink(index).attributes('href')).toBe(expectedLink);
+ },
+ );
+ });
+ });
+
+ describe('limit alert', () => {
+ it('displays limit alert when there is a next page', async () => {
+ createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findLink().attributes('href')).toBe(
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js
index 1c23a7e4fcf..a82390fae22 100644
--- a/spec/frontend/pipelines/utils_spec.js
+++ b/spec/frontend/pipelines/utils_spec.js
@@ -8,10 +8,14 @@ import {
removeOrphanNodes,
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
-import { createNodeDict } from '~/pipelines/utils';
+import { createNodeDict, calculateJobStats, calculateSlowestFiveJobs } from '~/pipelines/utils';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
-import { generateResponse, mockPipelineResponse } from './graph/mock_data';
+import {
+ generateResponse,
+ mockPipelineResponse,
+ mockPerformanceInsightsResponse,
+} from './graph/mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
@@ -158,4 +162,40 @@ describe('DAG visualization parsing utilities', () => {
expect(columns).toMatchSnapshot();
});
});
+
+ describe('performance insights', () => {
+ const {
+ data: {
+ project: {
+ pipeline: { jobs },
+ },
+ },
+ } = mockPerformanceInsightsResponse;
+
+ describe('calculateJobStats', () => {
+ const expectedJob = jobs.nodes[0];
+
+ it('returns the job that spent this longest time queued', () => {
+ expect(calculateJobStats(jobs, 'queuedDuration')).toEqual(expectedJob);
+ });
+
+ it('returns the job that was executed last', () => {
+ expect(calculateJobStats(jobs, 'startedAt')).toEqual(expectedJob);
+ });
+ });
+
+ describe('calculateSlowestFiveJobs', () => {
+ it('returns the slowest five jobs of the pipeline', () => {
+ const expectedJobs = [
+ jobs.nodes[9],
+ jobs.nodes[1],
+ jobs.nodes[5],
+ jobs.nodes[7],
+ jobs.nodes[8],
+ ];
+
+ expect(calculateSlowestFiveJobs(jobs)).toEqual(expectedJobs);
+ });
+ });
+ });
});
diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
index f7f5b2ca865..a9fe293dd69 100644
--- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
+++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb
@@ -143,6 +143,22 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do
end
end
end
+
+ context 'up to 15.1.0' do
+ let(:available_runner_releases) { %w[14.9.1 14.9.2 14.10.0 14.10.1 15.0.0 15.1.0] }
+
+ context 'with Gitlab::VERSION set to 15.2.0-pre' do
+ let(:gitlab_version) { '15.2.0-pre' }
+
+ context 'with unknown runner version' do
+ let(:runner_version) { '14.11.0~beta.29.gd0c550e3' }
+
+ it 'recommends 15.1.0 since 14.11 is an unknown release and 15.1.0 is available' do
+ is_expected.to eq({ recommended: Gitlab::VersionInfo.new(15, 1, 0) })
+ end
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index fac960d8df2..157cd408da9 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -383,21 +383,52 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
end
end
- it 'restores releases with links & milestones' do
- release = @project.releases.last
- link = release.links.last
+ context 'restores releases' do
+ it 'with links & milestones' do
+ release = @project.releases.last
+ link = release.links.last
+
+ aggregate_failures do
+ expect(release.tag).to eq('release-1.2')
+ expect(release.description).to eq('Some release notes')
+ expect(release.name).to eq('release-1.2')
+ expect(release.sha).to eq('903de3a8bd5573f4a049b1457d28bc1592ba6bf9')
+ expect(release.released_at).to eq('2019-12-27T10:17:14.615Z')
+ expect(release.milestone_releases.count).to eq(1)
+ expect(release.milestone_releases.first.milestone.title).to eq('test milestone')
+
+ expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download')
+ expect(link.name).to eq('release-1.2.dmg')
+ end
+ end
- aggregate_failures do
- expect(release.tag).to eq('release-1.1')
- expect(release.description).to eq('Some release notes')
- expect(release.name).to eq('release-1.1')
- expect(release.sha).to eq('901de3a8bd5573f4a049b1457d28bc1592ba6bf9')
- expect(release.released_at).to eq('2019-12-26T10:17:14.615Z')
- expect(release.milestone_releases.count).to eq(1)
- expect(release.milestone_releases.first.milestone.title).to eq('test milestone')
-
- expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download')
- expect(link.name).to eq('release-1.1.dmg')
+ context 'with author' do
+ it 'as ghost user when imported release author is empty' do
+ release = @project.releases.first
+
+ aggregate_failures do
+ expect(release.tag).to eq('release-1.0')
+ expect(release.author_id).to eq(User.select(:id).ghost.id)
+ end
+ end
+
+ it 'as existing member when imported release author is matched with existing user' do
+ release = @project.releases.second
+
+ aggregate_failures do
+ expect(release.tag).to eq('release-1.1')
+ expect(release.author_id).to eq(@existing_members.first.id)
+ end
+ end
+
+ it 'as import user when imported release author cannot be matched' do
+ release = @project.releases.last
+
+ aggregate_failures do
+ expect(release.tag).to eq('release-1.2')
+ expect(release.author_id).to eq(@user.id)
+ end
+ end
end
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index fb4d1cee606..fb3968777bf 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -78,6 +78,32 @@ RSpec.describe WebHook do
expect(hook.url).to eq('https://example.com')
end
+
+ context 'when there are URL variables' do
+ subject { hook }
+
+ before do
+ hook.url_variables = { 'one' => 'a', 'two' => 'b' }
+ end
+
+ it { is_expected.to allow_value('http://example.com').for(:url) }
+ it { is_expected.to allow_value('http://example.com/{one}/{two}').for(:url) }
+ it { is_expected.to allow_value('http://example.com/{one}').for(:url) }
+ it { is_expected.to allow_value('http://example.com/{two}').for(:url) }
+ it { is_expected.to allow_value('http://user:s3cret@example.com/{two}').for(:url) }
+ it { is_expected.to allow_value('http://{one}:{two}@example.com').for(:url) }
+
+ it { is_expected.not_to allow_value('http://example.com/{one}/{two}/{three}').for(:url) }
+ it { is_expected.not_to allow_value('http://example.com/{foo}').for(:url) }
+ it { is_expected.not_to allow_value('http:{user}:{pwd}//example.com/{foo}').for(:url) }
+
+ it 'mentions all missing variable names' do
+ hook.url = 'http://example.com/{one}/{foo}/{two}/{three}'
+
+ expect(hook).to be_invalid
+ expect(hook.errors[:url].to_sentence).to eq "Invalid URL template. Missing keys: [\"foo\", \"three\"]"
+ end
+ end
end
describe 'token' do
@@ -559,4 +585,54 @@ RSpec.describe WebHook do
expect(hook.to_json(unsafe_serialization_hash: true)).not_to include('encrypted_url_variables')
end
end
+
+ describe '#interpolated_url' do
+ subject(:hook) { build(:project_hook, project: project) }
+
+ context 'when the hook URL does not contain variables' do
+ before do
+ hook.url = 'http://example.com'
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: hook.url) }
+ end
+
+ it 'is not vulnerable to malicious input' do
+ hook.url = 'something%{%<foo>2147483628G}'
+ hook.url_variables = { 'foo' => '1234567890.12345678' }
+
+ expect(hook).to have_attributes(interpolated_url: hook.url)
+ end
+
+ context 'when the hook URL contains variables' do
+ before do
+ hook.url = 'http://example.com/{path}/resource?token={token}'
+ hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
+
+ context 'when a variable is missing' do
+ before do
+ hook.url_variables = { 'path' => 'present' }
+ end
+
+ it 'raises an error' do
+ # We expect validations to prevent this entirely - this is not user-error
+ expect { hook.interpolated_url }
+ .to raise_error(described_class::InterpolationError, include('Missing key token'))
+ end
+ end
+
+ context 'when the URL appears to include percent formatting' do
+ before do
+ hook.url = 'http://example.com/%{path}/resource?token=%{token}'
+ end
+
+ it 'succeeds, interpolates correctly' do
+ expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
+ end
+ end
+ end
+ end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 79edfdd2b3f..2bd23340a88 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -310,6 +310,26 @@ RSpec.describe "Authentication", "routing" do
expect(post("/users/auth/ldapmain/callback")).not_to be_routable
end
end
+
+ context 'with multiple LDAP providers configured' do
+ let(:ldap_settings) do
+ {
+ enabled: true,
+ servers: {
+ main: { 'provider_name' => 'ldapmain' },
+ secondary: { 'provider_name' => 'ldapsecondary' }
+ }
+ }
+ end
+
+ it 'POST /users/auth/ldapmain/callback' do
+ expect(post("/users/auth/ldapmain/callback")).to route_to('ldap/omniauth_callbacks#ldapmain')
+ end
+
+ it 'POST /users/auth/ldapsecondary/callback' do
+ expect(post("/users/auth/ldapsecondary/callback")).to route_to('ldap/omniauth_callbacks#ldapsecondary')
+ end
+ end
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 068550ec234..339ffc44e4d 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -84,8 +84,74 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
Gitlab::WebHooks::RecursionDetection.set_request_uuid(uuid)
end
+ context 'when there is an interpolation error' do
+ let(:error) { ::WebHook::InterpolationError.new('boom') }
+
+ before do
+ stub_full_request(project_hook.url, method: :post)
+ allow(project_hook).to receive(:interpolated_url).and_raise(error)
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error)
+
+ expect(service_instance).to receive(:log_execution).with(
+ execution_duration: (be > 0),
+ response: have_attributes(code: 200)
+ )
+
+ service_instance.execute
+ end
+ end
+
+ context 'when there are URL variables' do
+ before do
+ project_hook.update!(
+ url: 'http://example.com/{one}/{two}',
+ url_variables: { 'one' => 'a', 'two' => 'b' }
+ )
+ end
+
+ it 'POSTs to the interpolated URL, and logs the hook.url' do
+ stub_full_request(project_hook.interpolated_url, method: :post)
+
+ expect(service_instance).to receive(:queue_log_execution_with_retry).with(
+ include(url: project_hook.url),
+ :ok
+ )
+
+ service_instance.execute
+
+ expect(WebMock)
+ .to have_requested(:post, stubbed_hostname(project_hook.interpolated_url)).once
+ end
+
+ context 'there is userinfo' do
+ before do
+ project_hook.update!(url: 'http://{one}:{two}@example.com')
+ stub_full_request('http://example.com', method: :post)
+ end
+
+ it 'POSTs to the interpolated URL, and logs the hook.url' do
+ expect(service_instance).to receive(:queue_log_execution_with_retry).with(
+ include(url: project_hook.url),
+ :ok
+ )
+
+ service_instance.execute
+
+ expect(WebMock)
+ .to have_requested(:post, stubbed_hostname('http://example.com'))
+ .with(headers: headers.merge('Authorization' => 'Basic YTpi'))
+ .once
+ end
+ end
+ end
+
context 'when token is defined' do
- let_it_be(:project_hook) { create(:project_hook, :token) }
+ before do
+ project_hook.token = generate(:token)
+ end
it 'POSTs to the webhook URL' do
stub_full_request(project_hook.url, method: :post)
diff --git a/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
index 8635c9a8ff9..b31fe9ee0d1 100644
--- a/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
+++ b/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -14,6 +14,8 @@ RSpec.shared_context 'Ldap::OmniauthCallbacksController' do
{ main: ldap_config_defaults(:main) }
end
+ let(:multiple_ldap_servers_license_available) { true }
+
def ldap_config_defaults(key, hash = {})
{
provider_name: "ldap#{key}",
@@ -23,6 +25,7 @@ RSpec.shared_context 'Ldap::OmniauthCallbacksController' do
end
before do
+ stub_licensed_features(multiple_ldap_servers: multiple_ldap_servers_license_available)
stub_ldap_setting(ldap_settings)
described_class.define_providers!
Rails.application.reload_routes!
diff --git a/spec/views/layouts/_flash.html.haml_spec.rb b/spec/views/layouts/_flash.html.haml_spec.rb
index 82c06feb4fb..a4bed09368f 100644
--- a/spec/views/layouts/_flash.html.haml_spec.rb
+++ b/spec/views/layouts/_flash.html.haml_spec.rb
@@ -9,7 +9,11 @@ RSpec.describe 'layouts/_flash' do
end
describe 'closable flash messages' do
- %w(alert notice success).each do |flash_type|
+ where(:flash_type) do
+ %w[alert notice success]
+ end
+
+ with_them do
let(:flash) { { flash_type => 'This is a closable flash message' } }
it 'shows a close button' do
@@ -19,10 +23,14 @@ RSpec.describe 'layouts/_flash' do
end
describe 'non closable flash messages' do
- %w(error message toast warning).each do |flash_type|
+ where(:flash_type) do
+ %w[error message toast warning]
+ end
+
+ with_them do
let(:flash) { { flash_type => 'This is a non closable flash message' } }
- it 'shows a close button' do
+ it 'does not show a close button' do
expect(rendered).not_to include('js-close-icon')
end
end