Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-15 18:09:50 +00:00
parent b616fd825f
commit b302502690
80 changed files with 1577 additions and 176 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -1 +1 @@
1250b121b00ef5b3d637463cd4b9e5d93076f9b0
2ba30c8b1b5a428a645faf72881771af9a505ab2

View File

@ -1 +1 @@
1.59.0
1.61.0

View File

@ -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({

View File

@ -63,6 +63,7 @@ export function initIncidentApp(issueData = {}) {
return createElement(IssueApp, {
props: {
...issueData,
issueId: Number(issuableId),
issuableStatus: state,
descriptionComponent: IncidentTabs,
showTitleBorder: false,

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -109,3 +109,5 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-20p',
},
];
export const performanceModalId = 'performanceInsightsModal';

View File

@ -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
}
}
}
}
}

View File

@ -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);
};

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -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',
),

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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 || '')

View File

@ -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!')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
cb1457d19b058add7a966690f8d83e4f7e1612f2de3d6d8a87873bb7fb19960b

View File

@ -0,0 +1 @@
aa4e72f0f6596a609a7620c32e2a5def1ce3ee7200cf7513dd3c6569f68db342

View File

@ -0,0 +1 @@
e52d274075c18c3b80ed8306138eabd6dd3d1157dd1093f7f769b0a6cfb56791

View File

@ -0,0 +1 @@
b80d15b0176f0372a1553920ba72c43a2f9831f786358397f820a83b1b840cdc

View File

@ -0,0 +1 @@
6e59a39a5d843b5df3b33edb54c51f08062bff7ab1676b9326bb5aa8da159027

View File

@ -0,0 +1 @@
efdfa1c6ffb1b5e4de42bbfd87820eb5d1b87883c8b93cb4cb4101ba928f56dd

View File

@ -0,0 +1 @@
ecd71a6f9c90bd19a28edcd054ce2ef826859e051dd44c9fea875a5c32040a12

View File

@ -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);

View File

@ -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.

View File

@ -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:

View File

@ -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/).

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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**.

View File

@ -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.

View File

@ -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) | | | | ✓ | ✓ | ✓ |

View File

@ -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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -23,87 +23,127 @@ Additionally, you can integrate milestones with the [Releases feature](../releas
## Project milestones and group 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.
To view the project milestone list, in a project, go to **{issues}** **Issues > Milestones**.
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
## Creating 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
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**.
You can delete a milestone by selecting the **Delete** button.
## Delete a milestone
### Promoting project milestones to group milestones
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/343889) the minimum user role from Developer to Reporter in GitLab 15.0.
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**.
## 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.
![Promote milestone](img/milestones_promote_milestone.png)
Prerequisites:
## Assigning milestones from the sidebar
- You must have at least the Reporter role for the group.
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.
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.
To assign or unassign a milestone:
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.
You can also use the `/assign` [quick action](../quick_actions.md) in a comment.
## Filtering issues and merge requests by milestone

View File

@ -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**.

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -7,7 +7,7 @@ FactoryBot.define do
project
trait :token do
token { SecureRandom.hex(10) }
token { generate(:token) }
end
trait :all_events_enabled do

View File

@ -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

View File

@ -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": [

View File

@ -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}}]}

View File

@ -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');
});
});

View File

@ -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);

View File

@ -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',
});
});
});
});
});

View File

@ -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,
},
],
},
},
},
},
};

View File

@ -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',
);
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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

View File

@ -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.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')
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.1.dmg')
expect(link.url).to eq('http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download')
expect(link.name).to eq('release-1.2.dmg')
end
end
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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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!

View File

@ -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