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:
-rw-r--r--.rubocop_todo.yml7
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue15
-rw-r--r--app/assets/javascripts/frequent_items/utils.js4
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue48
-rw-r--r--app/assets/javascripts/incidents/constants.js8
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql9
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql4
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js13
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js4
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue81
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue7
-rw-r--r--app/assets/javascripts/packages/details/store/getters.js2
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue7
-rw-r--r--app/assets/javascripts/project_find_file.js2
-rw-r--r--app/assets/javascripts/user_popovers.js2
-rw-r--r--app/assets/stylesheets/framework/filters.scss17
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb3
-rw-r--r--app/graphql/types/alert_management/alert_type.rb10
-rw-r--r--app/graphql/types/ci/pipeline_type.rb2
-rw-r--r--app/graphql/types/countable_connection_type.rb (renamed from app/graphql/types/issuable_connection_type.rb)3
-rw-r--r--app/graphql/types/environment_type.rb5
-rw-r--r--app/graphql/types/issue_type.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb13
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/prometheus_alert_type.rb20
-rw-r--r--app/models/alert_management/alert.rb3
-rw-r--r--app/models/ci/job_artifact.rb11
-rw-r--r--app/models/concerns/file_store_mounter.rb21
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/lfs_object.rb11
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/prometheus_alert.rb1
-rw-r--r--app/models/terraform/state.rb11
-rw-r--r--app/policies/prometheus_alert_policy.rb5
-rw-r--r--app/presenters/alert_management/alert_presenter.rb10
-rw-r--r--app/presenters/prometheus_alert_presenter.rb18
-rw-r--r--app/serializers/environment_entity.rb6
-rw-r--r--changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml5
-rw-r--r--changelogs/unreleased/232580-state-count.yml5
-rw-r--r--changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/eb-skip-cobertura-sources.yml5
-rw-r--r--changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml5
-rw-r--r--changelogs/unreleased/group-coverage-reporting-csv.yml5
-rw-r--r--changelogs/unreleased/rails-save-bang-12.yml5
-rw-r--r--changelogs/unreleased/sabrams-fix_composer_installation_code.yml5
-rw-r--r--changelogs/unreleased/sh-refactor-file-store-mounter.yml5
-rw-r--r--changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml5
-rw-r--r--config/dependency_decisions.yml7
-rw-r--r--config/webpack.vendor.config.js2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql85
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json231
-rw-r--r--doc/api/graphql/reference/index.md14
-rw-r--r--doc/api/personal_access_tokens.md28
-rw-r--r--doc/api/vulnerabilities.md2
-rw-r--r--doc/api/vulnerability_findings.md2
-rw-r--r--doc/ci/parent_child_pipelines.md4
-rw-r--r--doc/development/documentation/styleguide.md8
-rw-r--r--doc/operations/incident_management/img/incident_list.pngbin0 -> 34194 bytes
-rw-r--r--doc/operations/incident_management/index.md24
-rw-r--r--doc/operations/metrics/alerts.md13
-rw-r--r--doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.pngbin34737 -> 0 bytes
-rw-r--r--doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.pngbin0 -> 14538 bytes
-rw-r--r--doc/operations/metrics/dashboards/index.md7
-rw-r--r--doc/operations/metrics/img/linked_runbooks_on_charts.pngbin0 -> 16966 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/vulnerability_page_v13_1.png (renamed from doc/user/application_security/security_dashboard/img/standalone_vulnerability_page_v13_1.png)bin79341 -> 79341 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md2
-rw-r--r--doc/user/application_security/vulnerabilities/img/vulnerability_page_download_patch_button_v13_1.png (renamed from doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_download_patch_button_v13_1.png)bin8979 -> 8979 bytes
-rw-r--r--doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_dropdown_v13_1.png (renamed from doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png)bin53561 -> 53561 bytes
-rw-r--r--doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_v13_1.png (renamed from doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_v13_1.png)bin15394 -> 15394 bytes
-rw-r--r--doc/user/application_security/vulnerabilities/img/vulnerability_page_v13_1.png (renamed from doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_v13_1.png)bin41387 -> 41387 bytes
-rw-r--r--doc/user/application_security/vulnerabilities/index.md12
-rw-r--r--doc/user/group/saml_sso/group_managed_accounts.md7
-rw-r--r--doc/user/project/integrations/webhooks.md52
-rw-r--r--doc/user/project/issues/design_management.md5
-rw-r--r--doc/user/project/merge_requests/fail_fast_testing.md13
-rw-r--r--doc/user/project/merge_requests/load_performance_testing.md3
-rw-r--r--doc/user/project/merge_requests/test_coverage_visualization.md4
-rw-r--r--lib/api/admin/ci/variables.rb18
-rw-r--r--lib/api/ci/pipeline_schedules.rb34
-rw-r--r--lib/api/ci/pipelines.rb28
-rw-r--r--lib/api/ci/runners.rb4
-rw-r--r--lib/api/entities/bridge.rb9
-rw-r--r--lib/api/entities/ci/bridge.rb11
-rw-r--r--lib/api/entities/ci/job.rb15
-rw-r--r--lib/api/entities/ci/job_artifact.rb11
-rw-r--r--lib/api/entities/ci/job_artifact_file.rb12
-rw-r--r--lib/api/entities/ci/job_basic.rb20
-rw-r--r--lib/api/entities/ci/job_basic_with_project.rb11
-rw-r--r--lib/api/entities/ci/pipeline.rb19
-rw-r--r--lib/api/entities/ci/pipeline_basic.rb16
-rw-r--r--lib/api/entities/ci/pipeline_schedule.rb14
-rw-r--r--lib/api/entities/ci/pipeline_schedule_details.rb12
-rw-r--r--lib/api/entities/ci/variable.rb14
-rw-r--r--lib/api/entities/commit_detail.rb2
-rw-r--r--lib/api/entities/deployment.rb2
-rw-r--r--lib/api/entities/job.rb13
-rw-r--r--lib/api/entities/job_artifact.rb9
-rw-r--r--lib/api/entities/job_artifact_file.rb10
-rw-r--r--lib/api/entities/job_basic.rb18
-rw-r--r--lib/api/entities/job_basic_with_project.rb9
-rw-r--r--lib/api/entities/job_request/dependency.rb2
-rw-r--r--lib/api/entities/merge_request.rb4
-rw-r--r--lib/api/entities/package/pipeline.rb2
-rw-r--r--lib/api/entities/pipeline.rb17
-rw-r--r--lib/api/entities/pipeline_basic.rb14
-rw-r--r--lib/api/entities/pipeline_schedule.rb12
-rw-r--r--lib/api/entities/pipeline_schedule_details.rb10
-rw-r--r--lib/api/entities/variable.rb12
-rw-r--r--lib/api/group_variables.rb18
-rw-r--r--lib/api/job_artifacts.rb4
-rw-r--r--lib/api/jobs.rb32
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/triggers.rb4
-rw-r--r--lib/api/variables.rb18
-rw-r--r--lib/gitlab/ci/parsers/coverage/cobertura.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml1
-rw-r--r--lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml2
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb2
-rw-r--r--locale/gitlab.pot36
-rw-r--r--package.json2
-rw-r--r--spec/factories/merge_requests.rb15
-rw-r--r--spec/fixtures/api/schemas/environment.json1
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js26
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js44
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js14
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_fixtures.js114
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_tests.js68
-rw-r--r--spec/frontend/notebook/cells/output/html_spec.js17
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js2
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js20
-rw-r--r--spec/frontend/packages/shared/components/package_list_row_spec.js8
-rw-r--r--spec/frontend/project_find_file_spec.js6
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb2
-rw-r--r--spec/graphql/types/countable_connection_type_spec.rb (renamed from spec/graphql/types/issuable_connection_type_spec.rb)0
-rw-r--r--spec/graphql/types/environment_type_spec.rb67
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb4
-rw-r--r--spec/graphql/types/project_type_spec.rb9
-rw-r--r--spec/graphql/types/prometheus_alert_type_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb35
-rw-r--r--spec/models/alert_management/alert_spec.rb11
-rw-r--r--spec/models/ci/job_artifact_spec.rb12
-rw-r--r--spec/models/environment_spec.rb24
-rw-r--r--spec/models/lfs_object_spec.rb16
-rw-r--r--spec/models/terraform/state_spec.rb8
-rw-r--r--spec/presenters/alert_management/alert_presenter_spec.rb6
-rw-r--r--spec/presenters/prometheus_alert_presenter_spec.rb32
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb39
-rw-r--r--spec/serializers/environment_entity_spec.rb20
-rw-r--r--spec/services/todo_service_spec.rb55
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/design_management_test_helpers.rb4
-rw-r--r--spec/support/helpers/jira_service_helper.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/notification_helpers.rb4
-rw-r--r--spec/support/helpers/stub_object_storage.rb2
-rw-r--r--spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb11
-rw-r--r--spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb17
-rw-r--r--yarn.lock5
163 files changed, 1685 insertions, 560 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 21784a993b1..5a1fc68a915 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -862,7 +862,6 @@ Rails/SaveBang:
- 'ee/spec/services/todo_service_spec.rb'
- 'ee/spec/services/update_build_minutes_service_spec.rb'
- 'ee/spec/services/vulnerability_feedback/create_service_spec.rb'
- - 'ee/spec/support/helpers/ee/geo_helpers.rb'
- 'ee/spec/support/protected_tags/access_control_shared_examples.rb'
- 'ee/spec/support/shared_examples/features/protected_branches_access_control_shared_examples.rb'
- 'ee/spec/support/shared_examples/finders/geo/framework_registry_finder_shared_examples.rb'
@@ -1306,12 +1305,6 @@ Rails/SaveBang:
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
- - 'spec/support/helpers/cycle_analytics_helpers.rb'
- - 'spec/support/helpers/design_management_test_helpers.rb'
- - 'spec/support/helpers/jira_service_helper.rb'
- - 'spec/support/helpers/login_helpers.rb'
- - 'spec/support/helpers/notification_helpers.rb'
- - 'spec/support/helpers/stub_object_storage.rb'
- 'spec/support/migrations_helpers/cluster_helpers.rb'
- 'spec/support/migrations_helpers/namespaces_helper.rb'
- 'spec/support/shared_contexts/email_shared_context.rb'
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 14c71f73291..fbf19847e9d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -3,7 +3,6 @@ import {
GlAlert,
GlButton,
GlCollapse,
- GlDeprecatedButton,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
@@ -39,7 +38,6 @@ export default {
GlAlert,
GlButton,
GlCollapse,
- GlDeprecatedButton,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
@@ -340,24 +338,25 @@ export default {
</gl-alert>
</gl-collapse>
<template #modal-footer>
- <gl-deprecated-button @click="hideModal">{{ __('Cancel') }}</gl-deprecated-button>
- <gl-deprecated-button
+ <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
+ <gl-button
v-if="variableBeingEdited"
ref="deleteCiVariable"
- category="secondary"
variant="danger"
+ category="secondary"
data-qa-selector="ci_variable_delete_button"
@click="deleteVarAndClose"
- >{{ __('Delete variable') }}</gl-deprecated-button
+ >{{ __('Delete variable') }}</gl-button
>
- <gl-deprecated-button
+ <gl-button
ref="updateOrAddVariable"
:disabled="!canSubmit"
variant="success"
+ category="primary"
data-qa-selector="ci_variable_save_button"
@click="updateOrAddVariable"
>{{ modalActionText }}
- </gl-deprecated-button>
+ </gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index d4b7ffdcbe1..112e8eaaf17 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -1,6 +1,6 @@
import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
@@ -52,7 +52,7 @@ export const sanitizeItem = item => {
return {};
}
- return { [key]: sanitize(item[key].toString(), { allowedTags: [] }) };
+ return { [key]: sanitize(item[key].toString(), { ALLOWED_TAGS: [] }) };
};
return {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 85f24c5b122..ecd8acb449e 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -13,6 +13,7 @@ import {
GlPagination,
GlTabs,
GlTab,
+ GlBadge,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -20,7 +21,8 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
-import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATE_TABS } from '../constants';
+import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
+import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
const tdClass =
@@ -39,7 +41,7 @@ const initialPaginationState = {
export default {
i18n: I18N,
- stateTabs: INCIDENT_STATE_TABS,
+ statusTabs: INCIDENT_STATUS_TABS,
fields: [
{
key: 'title',
@@ -77,6 +79,7 @@ export default {
GlTabs,
GlTab,
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -94,7 +97,7 @@ export default {
variables() {
return {
searchTerm: this.searchTerm,
- state: this.stateFilter,
+ status: this.statusFilter,
projectPath: this.projectPath,
issueTypes: ['INCIDENT'],
sort: this.sort,
@@ -114,6 +117,19 @@ export default {
this.errored = true;
},
},
+ incidentsCount: {
+ query: getIncidentsCountByStatus,
+ variables() {
+ return {
+ searchTerm: this.searchTerm,
+ projectPath: this.projectPath,
+ issueTypes: ['INCIDENT'],
+ };
+ },
+ update(data) {
+ return data.project?.issueStatusCounts;
+ },
+ },
},
data() {
return {
@@ -123,15 +139,16 @@ export default {
searchTerm: '',
pagination: initialPaginationState,
incidents: {},
- stateFilter: '',
sort: 'created_desc',
sortBy: 'createdAt',
sortDesc: true,
+ statusFilter: '',
+ filteredByStatus: '',
};
},
computed: {
showErrorMsg() {
- return this.errored && !this.isErrorAlertDismissed && !this.searchTerm;
+ return this.errored && !this.isErrorAlertDismissed && this.incidentsCount?.all === 0;
},
loading() {
return this.$apollo.queries.incidents.loading;
@@ -139,6 +156,9 @@ export default {
hasIncidents() {
return this.incidents?.list?.length;
},
+ incidentsForCurrentTab() {
+ return this.incidentsCount?.[this.filteredByStatus.toLowerCase()] ?? 0;
+ },
showPaginationControls() {
return Boolean(
this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage,
@@ -149,7 +169,9 @@ export default {
},
nextPage() {
const nextPage = this.pagination.currentPage + 1;
- return this.incidents?.list?.length < DEFAULT_PAGE_SIZE ? null : nextPage;
+ return nextPage > Math.ceil(this.incidentsForCurrentTab / DEFAULT_PAGE_SIZE)
+ ? null
+ : nextPage;
},
tbodyTrClass() {
return {
@@ -181,9 +203,10 @@ export default {
this.searchTerm = trimmedInput;
}
}, INCIDENT_SEARCH_DELAY),
- filterIncidentsByState(tabIndex) {
- const { filters } = this.$options.stateTabs[tabIndex];
- this.stateFilter = filters;
+ filterIncidentsByStatus(tabIndex) {
+ const { filters, status } = this.$options.statusTabs[tabIndex];
+ this.statusFilter = filters;
+ this.filteredByStatus = status;
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
@@ -231,10 +254,13 @@ export default {
<div
class="incident-management-list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100"
>
- <gl-tabs content-class="gl-p-0" @input="filterIncidentsByState">
- <gl-tab v-for="tab in $options.stateTabs" :key="tab.state" :data-testid="tab.state">
+ <gl-tabs content-class="gl-p-0" @input="filterIncidentsByStatus">
+ <gl-tab v-for="tab in $options.statusTabs" :key="tab.status" :data-testid="tab.status">
<template #title>
<span>{{ tab.title }}</span>
+ <gl-badge v-if="incidentsCount" pill size="sm" class="gl-tab-counter-badge">
+ {{ incidentsCount[tab.status.toLowerCase()] }}
+ </gl-badge>
</template>
</gl-tab>
</gl-tabs>
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index fe92f131738..dc90f30991c 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -9,20 +9,20 @@ export const I18N = {
searchPlaceholder: __('Search results…'),
};
-export const INCIDENT_STATE_TABS = [
+export const INCIDENT_STATUS_TABS = [
{
title: s__('IncidentManagement|Open'),
- state: 'OPENED',
+ status: 'OPENED',
filters: 'opened',
},
{
title: s__('IncidentManagement|Closed'),
- state: 'CLOSED',
+ status: 'CLOSED',
filters: 'closed',
},
{
title: s__('IncidentManagement|All'),
- state: 'ALL',
+ status: 'ALL',
filters: 'all',
},
];
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
new file mode 100644
index 00000000000..0b784b104a8
--- /dev/null
+++ b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
@@ -0,0 +1,9 @@
+query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) {
+ project(fullPath: $projectPath) {
+ issueStatusCounts(search: $searchTerm, types: $issueTypes) {
+ all
+ opened
+ closed
+ }
+ }
+}
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
index 6e8e6a1254c..0f56e8640bd 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -2,7 +2,7 @@ query getIncidents(
$projectPath: ID!
$issueTypes: [IssueType!]
$sort: IssueSort
- $state: IssuableState
+ $status: IssuableState
$firstPageSize: Int
$lastPageSize: Int
$prevPageCursor: String = ""
@@ -12,9 +12,9 @@ query getIncidents(
project(fullPath: $projectPath) {
issues(
search: $searchTerm
- state: $state
types: $issueTypes
sort: $sort
+ state: $status
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index 05e384adad3..8cd1c1b0e56 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -1,4 +1,4 @@
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
export const parseIssuableData = () => {
try {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 08daac15754..e26b63fbb85 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -710,3 +710,16 @@ export const dateFromParams = (year, month, day) => {
return date;
};
+
+/**
+ * A utility function which computes the difference in seconds
+ * between 2 dates.
+ *
+ * @param {Date} startDate the start sate
+ * @param {Date} endDate the end date
+ *
+ * @return {Int} the difference in seconds
+ */
+export const differenceInSeconds = (startDate, endDate) => {
+ return (endDate.getTime() - startDate.getTime()) / 1000;
+};
diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js
index b1dd562f63a..32553af9af3 100644
--- a/app/assets/javascripts/lib/utils/highlight.js
+++ b/app/assets/javascripts/lib/utils/highlight.js
@@ -1,5 +1,5 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.
@@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
return string;
}
- const sanitizedValue = sanitize(string.toString(), { allowedTags: [] });
+ const sanitizedValue = sanitize(string.toString(), { ALLOWED_TAGS: [] });
// occurrences is an array of character indices that should be
// highlighted in the original string, i.e. [3, 4, 5, 7]
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index fcb09ea90db..fa1afdcd16f 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,6 +1,6 @@
<script>
import marked from 'marked';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import katex from 'katex';
import Prompt from './prompt.vue';
@@ -104,65 +104,58 @@ export default {
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
// allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html
- allowedTags: [
+ ALLOWED_TAGS: [
+ 'a',
+ 'abbr',
+ 'b',
+ 'blockquote',
+ 'br',
+ 'code',
+ 'dd',
+ 'del',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
- 'h7',
- 'h8',
- 'br',
- 'b',
+ 'hr',
'i',
- 'strong',
- 'em',
- 'a',
- 'pre',
- 'code',
'img',
- 'tt',
- 'div',
'ins',
- 'del',
- 'sup',
- 'sub',
- 'p',
- 'ol',
- 'ul',
- 'table',
- 'thead',
- 'tbody',
- 'tfoot',
- 'blockquote',
- 'dl',
- 'dt',
- 'dd',
'kbd',
+ 'li',
+ 'ol',
+ 'p',
+ 'pre',
'q',
- 'samp',
- 'var',
- 'hr',
- 'ruby',
- 'rt',
'rp',
- 'li',
- 'tr',
- 'td',
- 'th',
+ 'rt',
+ 'ruby',
's',
- 'strike',
+ 'samp',
'span',
- 'abbr',
- 'abbr',
+ 'strike',
+ 'strong',
+ 'sub',
'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr',
+ 'tt',
+ 'ul',
+ 'var',
],
- allowedAttributes: {
- '*': ['class', 'style'],
- a: ['href'],
- img: ['src'],
- },
+ ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
});
},
},
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index 8dc2d73af9b..b36761993ea 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,5 +1,5 @@
<script>
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import Prompt from '../prompt.vue';
export default {
@@ -23,10 +23,7 @@ export default {
computed: {
sanitizedOutput() {
return sanitize(this.rawCode, {
- allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']),
- allowedAttributes: {
- img: ['src'],
- },
+ ALLOWED_ATTR: ['src'],
});
},
showOutput() {
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
index 77dc24ff169..d1814d506ad 100644
--- a/app/assets/javascripts/packages/details/store/getters.js
+++ b/app/assets/javascripts/packages/details/store/getters.js
@@ -110,6 +110,6 @@ export const composerRegistryInclude = ({ composerPath }) => {
return JSON.stringify(base);
};
export const composerPackageInclude = ({ packageEntity }) => {
- const base = { package_name: packageEntity.name };
+ const base = { [packageEntity.name]: packageEntity.version };
return JSON.stringify(base);
};
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
index 3515ab4ef03..8cc07632695 100644
--- a/app/assets/javascripts/packages/shared/components/package_list_row.vue
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -54,9 +54,6 @@ export default {
hasProjectLink() {
return Boolean(this.packageEntity.project_path);
},
- deleteAvailable() {
- return !this.disableDelete && !this.isGroup;
- },
},
};
</script>
@@ -111,7 +108,7 @@ export default {
<div
class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
- :class="!deleteAvailable ? 'section-50' : 'section-40'"
+ :class="disableDelete ? 'section-50' : 'section-40'"
>
<publish-method :package-entity="packageEntity" :is-group="isGroup" />
@@ -126,7 +123,7 @@ export default {
</div>
</div>
- <div v-if="deleteAvailable" class="table-section section-10 d-flex justify-content-end">
+ <div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end">
<gl-button
data-testid="action-delete"
icon="remove"
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index a31034361a8..599888b1fe0 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 290de55e6f9..c8f95dac48e 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 8f209d2d99a..ee83ce67c03 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -458,6 +458,23 @@
}
.vue-filtered-search-bar-container {
+ .gl-search-box-by-click {
+ // Absolute width is needed to prevent flex to grow
+ // beyond the available width.
+ .gl-filtered-search-scrollable {
+ width: 1px;
+ }
+
+ // There are several styling issues happening while using
+ // `GlFilteredSearch` in roadmap due to some of our global
+ // styles which we need to override until those are fixed
+ // at framework level.
+ // See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/908
+ .input-group-prepend + .gl-filtered-search-scrollable {
+ border-radius: 0;
+ }
+ }
+
@include media-breakpoint-up(md) {
.sort-dropdown-container {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 48c6db51103..02f81157fce 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -446,6 +446,8 @@ $context-header-height: 60px;
$breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
+$issuable-title-max-width: 350px;
+$milestone-title-max-width: 75px;
$gl-line-height: 16px;
$gl-line-height-18: 18px;
$gl-line-height-20: 20px;
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 7ed88be52b9..0c01efd4f9a 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -38,6 +38,9 @@ module ResolvesMergeRequests
assignees: [:assignees],
labels: [:labels],
author: [:author],
+ merged_at: [:metrics],
+ commit_count: [:metrics],
+ approved_by: [:approver_users],
milestone: [:milestone],
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
}
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index 2e9b729a0b3..1a0b0685ffe 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -107,6 +107,16 @@ module Types
description: 'Todos of the current user for the alert',
resolver: Resolvers::TodoResolver
+ field :details_url,
+ GraphQL::STRING_TYPE,
+ null: false,
+ description: 'The URL of the alert detail page'
+
+ field :prometheus_alert,
+ Types::PrometheusAlertType,
+ null: true,
+ description: 'The alert condition for Prometheus'
+
def notes
object.ordered_notes
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 179a5393b17..caa7079e2c6 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -5,6 +5,8 @@ module Types
class PipelineType < BaseObject
graphql_name 'Pipeline'
+ connection_type_class(Types::CountableConnectionType)
+
authorize :read_pipeline
expose_permissions Types::PermissionTypes::Ci::Pipeline
diff --git a/app/graphql/types/issuable_connection_type.rb b/app/graphql/types/countable_connection_type.rb
index ec180cc1576..2538366b786 100644
--- a/app/graphql/types/issuable_connection_type.rb
+++ b/app/graphql/types/countable_connection_type.rb
@@ -2,13 +2,14 @@
module Types
# rubocop: disable Graphql/AuthorizeTypes
- class IssuableConnectionType < GraphQL::Types::Relay::BaseConnection
+ class CountableConnectionType < GraphQL::Types::Relay::BaseConnection
field :count, Integer, null: false,
description: 'Total count of collection'
def count
# rubocop: disable CodeReuse/ActiveRecord
relation = object.items
+
# sometimes relation is an Array
relation = relation.reorder(nil) if relation.respond_to?(:reorder)
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 34a90006d03..239b26f9c38 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -19,5 +19,10 @@ module Types
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver
+
+ field :latest_opened_most_severe_alert,
+ Types::AlertManagement::AlertType,
+ null: true,
+ description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.'
end
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index a5059276e84..0a73ce95424 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -4,7 +4,7 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
- connection_type_class(Types::IssuableConnectionType)
+ connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index c58f76ddb5c..01b02b7976f 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -4,7 +4,7 @@ module Types
class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
- connection_type_class(Types::IssuableConnectionType)
+ connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
@@ -143,6 +143,8 @@ module Types
end
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: Types::TaskCompletionStatus.description
+ field :commit_count, GraphQL::INT_TYPE, null: true,
+ description: 'Number of commits in the merge request'
def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a)
@@ -162,5 +164,14 @@ module Types
hash.merge!(additions: status.additions, deletions: status.deletions, file_count: 1) { |_, x, y| x + y }
end
end
+
+ def commit_count
+ object&.metrics&.commits_count
+ end
+
+ def approvers
+ object.approver_users
+ end
end
end
+Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index b0778a07214..523e019b955 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -169,6 +169,12 @@ module Types
description: 'Environments of the project',
resolver: Resolvers::EnvironmentsResolver
+ field :environment,
+ Types::EnvironmentType,
+ null: true,
+ description: 'A single environment of the project',
+ resolver: Resolvers::EnvironmentsResolver.single
+
field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true,
description: 'SAST CI configuration for the project',
resolver: ::Resolvers::CiConfiguration::SastResolver
diff --git a/app/graphql/types/prometheus_alert_type.rb b/app/graphql/types/prometheus_alert_type.rb
new file mode 100644
index 00000000000..1d09a8dbeb7
--- /dev/null
+++ b/app/graphql/types/prometheus_alert_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ class PrometheusAlertType < BaseObject
+ graphql_name 'PrometheusAlert'
+ description 'The alert condition for Prometheus'
+
+ authorize :read_prometheus_alerts
+
+ present_using PrometheusAlertPresenter
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the alert condition'
+
+ field :humanized_text,
+ GraphQL::STRING_TYPE,
+ null: false,
+ description: 'The human-readable text of the alert condition'
+ end
+end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 397c09f38fc..75581805b49 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -118,7 +118,7 @@ module AlertManagement
end
delegate :iid, to: :issue, prefix: true, allow_nil: true
- delegate :metrics_dashboard_url, :runbook, to: :present
+ delegate :metrics_dashboard_url, :runbook, :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { where(status: status) }
@@ -137,6 +137,7 @@ module AlertManagement
# Descending sort order sorts severity from more critical to less critical.
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
+ scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index de7bd9fb67b..75c3ce98c95 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -9,6 +9,7 @@ module Ci
include Sortable
include IgnorableColumns
include Artifactable
+ include FileStoreMounter
extend Gitlab::Ci::Model
NotSupportedAdapterError = Class.new(StandardError)
@@ -115,7 +116,7 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
- mount_uploader :file, JobArtifactUploader
+ mount_file_store_uploader JobArtifactUploader
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_supported_file_format!, on: :create
@@ -124,8 +125,6 @@ module Ci
update_project_statistics project_statistics_name: :build_artifacts_size
- after_save :update_file_store, if: :saved_change_to_file?
-
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :with_files_stored_locally, -> { where(file_store: ::JobArtifactUploader::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
@@ -229,12 +228,6 @@ module Ci
end
end
- def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
- end
-
def self.associated_file_types_for(file_type)
return unless file_types.include?(file_type)
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
new file mode 100644
index 00000000000..9d4463e5297
--- /dev/null
+++ b/app/models/concerns/file_store_mounter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module FileStoreMounter
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def mount_file_store_uploader(uploader)
+ mount_uploader(:file, uploader)
+
+ after_save :update_file_store, if: :saved_change_to_file?
+ end
+ end
+
+ private
+
+ def update_file_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 87587bb5afa..d6508ffceba 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -148,7 +148,7 @@ class Deployment < ApplicationRecord
def execute_hooks
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
- project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project)
+ project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project, default_enabled: true)
project.execute_services(deployment_data, :deployment_hooks)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index bddc84f10b5..c6a08c996da 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -29,6 +29,7 @@ class Environment < ApplicationRecord
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
+ has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -291,6 +292,10 @@ class Environment < ApplicationRecord
!!ENV['USE_SAMPLE_METRICS']
end
+ def has_opened_alert?
+ latest_opened_most_severe_alert.present?
+ end
+
def metrics
prometheus_adapter.query(:environment, self) if has_metrics_and_can_query?
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 3761484b15d..d60baa299cb 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -5,6 +5,7 @@ class LfsObject < ApplicationRecord
include Checksummable
include EachBatch
include ObjectStorage::BackgroundMove
+ include FileStoreMounter
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { distinct }, through: :lfs_objects_projects
@@ -15,21 +16,13 @@ class LfsObject < ApplicationRecord
validates :oid, presence: true, uniqueness: true
- mount_uploader :file, LfsObjectUploader
-
- after_save :update_file_store, if: :saved_change_to_file?
+ mount_file_store_uploader LfsObjectUploader
def self.not_linked_to_project(project)
where('NOT EXISTS (?)',
project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
end
- def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
- end
-
def project_allowed_access?(project)
if project.fork_network_member
lfs_objects_projects
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 1c95789d7ba..f4c2d568b4d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -264,10 +264,14 @@ class MergeRequest < ApplicationRecord
end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
scope :preload_source_project, -> { preload(:source_project) }
+ scope :preload_target_project, -> { preload(:target_project) }
scope :preload_routables, -> do
preload(target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
end
+ scope :preload_author, -> { preload(:author) }
+ scope :preload_approved_by_users, -> { preload(:approved_by_users) }
+ scope :preload_metrics, -> (relation) { preload(metrics: relation) }
scope :with_auto_merge_enabled, -> do
with_state(:opened).where(auto_merge_enabled: true)
diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb
index 1c870f4391a..80eef1705e7 100644
--- a/app/models/prometheus_alert.rb
+++ b/app/models/prometheus_alert.rb
@@ -3,6 +3,7 @@
class PrometheusAlert < ApplicationRecord
include Sortable
include UsageStatistics
+ include Presentable
OPERATORS_MAP = {
lt: "<",
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 759b9ce1eec..c50b9da1310 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -3,6 +3,7 @@
module Terraform
class State < ApplicationRecord
include UsageStatistics
+ include FileStoreMounter
DEFAULT = '{"version":1}'.freeze
HEX_REGEXP = %r{\A\h+\z}.freeze
@@ -17,18 +18,10 @@ module Terraform
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
- after_save :update_file_store, if: :saved_change_to_file?
-
- mount_uploader :file, StateUploader
+ mount_file_store_uploader StateUploader
default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) }
- def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
- end
-
def file_store
super || StateUploader.default_store
end
diff --git a/app/policies/prometheus_alert_policy.rb b/app/policies/prometheus_alert_policy.rb
new file mode 100644
index 00000000000..e6b0e6e8c17
--- /dev/null
+++ b/app/policies/prometheus_alert_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class PrometheusAlertPolicy < ::BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index c3067e6377f..5bfa6dee18b 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -4,6 +4,7 @@ module AlertManagement
class AlertPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
+ include ActionView::Helpers::UrlHelper
MARKDOWN_LINE_BREAK = " \n".freeze
@@ -45,15 +46,12 @@ module AlertManagement
def metrics_dashboard_url; end
- private
-
def details_url
- ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
- project,
- alert.iid
- )
+ details_project_alert_management_url(project, alert.iid)
end
+ private
+
attr_reader :alert, :project
def alerting_alert
diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb
new file mode 100644
index 00000000000..99e24bdcdb9
--- /dev/null
+++ b/app/presenters/prometheus_alert_presenter.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
+ include ActionView::Helpers::UrlHelper
+
+ presents :prometheus_alert
+
+ def humanized_text
+ operator_text =
+ case prometheus_alert.operator
+ when 'lt' then s_('PrometheusAlerts|is less than')
+ when 'eq' then s_('PrometheusAlerts|is equal to')
+ when 'gt' then s_('PrometheusAlerts|exceeded')
+ end
+
+ "#{operator_text} #{prometheus_alert.threshold}#{prometheus_alert.prometheus_metric.unit}"
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 7da5910a75b..a2bf9716f8f 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -71,6 +71,8 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :destroy_environment, environment)
end
+ expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert
+
private
alias_method :environment, :object
@@ -91,6 +93,10 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :read_pod_logs, environment.project)
end
+ def can_read_alert_management_alert?
+ can?(current_user, :read_alert_management_alert, environment.project)
+ end
+
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
diff --git a/changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml b/changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml
new file mode 100644
index 00000000000..e40960f1e6c
--- /dev/null
+++ b/changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml
@@ -0,0 +1,5 @@
+---
+title: Enable delete button on Package group level view list
+merge_request: 39430
+author:
+type: changed
diff --git a/changelogs/unreleased/232580-state-count.yml b/changelogs/unreleased/232580-state-count.yml
new file mode 100644
index 00000000000..9fc05eecdab
--- /dev/null
+++ b/changelogs/unreleased/232580-state-count.yml
@@ -0,0 +1,5 @@
+---
+title: Add incident count badge to the incident list
+merge_request: 38278
+author:
+type: changed
diff --git a/changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml b/changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml
new file mode 100644
index 00000000000..85f0dd4080f
--- /dev/null
+++ b/changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Expose counts (pipeline, commits) and approvers for a merge request in GraphQL
+merge_request: 39086
+author:
+type: added
diff --git a/changelogs/unreleased/eb-skip-cobertura-sources.yml b/changelogs/unreleased/eb-skip-cobertura-sources.yml
new file mode 100644
index 00000000000..fe0ed7d3648
--- /dev/null
+++ b/changelogs/unreleased/eb-skip-cobertura-sources.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore the sources node from the cobertura XML
+merge_request: 39385
+author:
+type: fixed
diff --git a/changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml b/changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml
new file mode 100644
index 00000000000..5f60e9b6c51
--- /dev/null
+++ b/changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Expose alert information for environments
+merge_request: 38881
+author:
+type: added
diff --git a/changelogs/unreleased/group-coverage-reporting-csv.yml b/changelogs/unreleased/group-coverage-reporting-csv.yml
new file mode 100644
index 00000000000..9f9b0546e24
--- /dev/null
+++ b/changelogs/unreleased/group-coverage-reporting-csv.yml
@@ -0,0 +1,5 @@
+---
+title: Add CoverageReportsController#index CSV response
+merge_request: 38520
+author:
+type: added
diff --git a/changelogs/unreleased/rails-save-bang-12.yml b/changelogs/unreleased/rails-save-bang-12.yml
new file mode 100644
index 00000000000..5e4f8ccf900
--- /dev/null
+++ b/changelogs/unreleased/rails-save-bang-12.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor spec/support/helpers/* and ee/spec/support/helpers/* to fix Rails/SaveBang Cop
+merge_request: 38995
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/sabrams-fix_composer_installation_code.yml b/changelogs/unreleased/sabrams-fix_composer_installation_code.yml
new file mode 100644
index 00000000000..367d8a73baa
--- /dev/null
+++ b/changelogs/unreleased/sabrams-fix_composer_installation_code.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Composer installation code snippet to include package name and version
+merge_request: 39400
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-refactor-file-store-mounter.yml b/changelogs/unreleased/sh-refactor-file-store-mounter.yml
new file mode 100644
index 00000000000..19d8b346ab4
--- /dev/null
+++ b/changelogs/unreleased/sh-refactor-file-store-mounter.yml
@@ -0,0 +1,5 @@
+---
+title: Move file store updates and mount_uploader into a concern
+merge_request: 37907
+author:
+type: other
diff --git a/changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml b/changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml
new file mode 100644
index 00000000000..1a565bba4ea
--- /dev/null
+++ b/changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml
@@ -0,0 +1,5 @@
+---
+title: Specify Ruby image in FailFast template
+merge_request: 38523
+author:
+type: changed
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 790b09c1dfa..d6386329d83 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -639,3 +639,10 @@
:why: MIT license
:versions: []
:when: 2020-07-28 20:35:27.574875000 Z
+- - :license
+ - dompurify
+ - Apache-2.0
+ - :who: Lukas Eipert
+ :why: "https://github.com/cure53/DOMPurify/blob/main/LICENSE and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31928#note_346604841"
+ :versions: []
+ :when: 2020-08-13 13:42:46.508082000 Z
diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js
index 548eca4200f..29c4c33314e 100644
--- a/config/webpack.vendor.config.js
+++ b/config/webpack.vendor.config.js
@@ -40,7 +40,7 @@ module.exports = {
'select2',
'moment-mini',
'aws-sdk',
- 'sanitize-html',
+ 'dompurify',
'bootstrap/dist/js/bootstrap.js',
'sortablejs/modular/sortable.esm.js',
'popper.js',
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 82344869fa1..92766ab68e4 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -210,6 +210,11 @@ type AlertManagementAlert implements Noteable {
details: JSON
"""
+ The URL of the alert detail page
+ """
+ detailsUrl: String!
+
+ """
All discussions on this noteable
"""
discussions(
@@ -295,6 +300,11 @@ type AlertManagementAlert implements Noteable {
): NoteConnection!
"""
+ The alert condition for Prometheus
+ """
+ prometheusAlert: PrometheusAlert
+
+ """
Runbook for the alert as defined in alert details
"""
runbook: String
@@ -4419,6 +4429,11 @@ type Environment {
id: ID!
"""
+ The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.
+ """
+ latestOpenedMostSevereAlert: AlertManagementAlert
+
+ """
Metrics dashboard schema for the environment
"""
metricsDashboard(
@@ -8241,6 +8256,31 @@ type MergeRequest implements Noteable {
allowCollaboration: Boolean
"""
+ Users who approved the merge request
+ """
+ approvedBy(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): UserConnection
+
+ """
Assignees of the merge request
"""
assignees(
@@ -8271,6 +8311,11 @@ type MergeRequest implements Noteable {
author: User
"""
+ Number of commits in the merge request
+ """
+ commitCount: Int
+
+ """
Timestamp of when the merge request was created
"""
createdAt: Time!
@@ -10250,6 +10295,11 @@ The connection type for Pipeline.
"""
type PipelineConnection {
"""
+ Total count of collection
+ """
+ count: Int!
+
+ """
A list of edges.
"""
edges: [PipelineEdge]
@@ -10554,6 +10604,26 @@ type Project {
descriptionHtml: String
"""
+ A single environment of the project
+ """
+ environment(
+ """
+ Name of the environment
+ """
+ name: String
+
+ """
+ Search query for environment name
+ """
+ search: String
+
+ """
+ States of environments that should be included in result
+ """
+ states: [String!]
+ ): Environment
+
+ """
Environments of the project
"""
environments(
@@ -12116,6 +12186,21 @@ type ProjectStatistics {
wikiSize: Float
}
+"""
+The alert condition for Prometheus
+"""
+type PrometheusAlert {
+ """
+ The human-readable text of the alert condition
+ """
+ humanizedText: String!
+
+ """
+ ID of the alert condition
+ """
+ id: ID!
+}
+
type Query {
"""
Get information about current user
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 0f13079f202..719d448b5f4 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -578,6 +578,24 @@
"deprecationReason": null
},
{
+ "name": "detailsUrl",
+ "description": "The URL of the alert detail page",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "discussions",
"description": "All discussions on this noteable",
"args": [
@@ -802,6 +820,20 @@
"deprecationReason": null
},
{
+ "name": "prometheusAlert",
+ "description": "The alert condition for Prometheus",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PrometheusAlert",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "runbook",
"description": "Runbook for the alert as defined in alert details",
"args": [
@@ -12339,6 +12371,20 @@
"deprecationReason": null
},
{
+ "name": "latestOpenedMostSevereAlert",
+ "description": "The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "metricsDashboard",
"description": "Metrics dashboard schema for the environment",
"args": [
@@ -22903,6 +22949,59 @@
"deprecationReason": null
},
{
+ "name": "approvedBy",
+ "description": "Users who approved the merge request",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "assignees",
"description": "Assignees of the merge request",
"args": [
@@ -22970,6 +23069,20 @@
"deprecationReason": null
},
{
+ "name": "commitCount",
+ "description": "Number of commits in the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "createdAt",
"description": "Timestamp of when the merge request was created",
"args": [
@@ -30646,6 +30759,24 @@
"description": "The connection type for Pipeline.",
"fields": [
{
+ "name": "count",
+ "description": "Total count of collection",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "edges",
"description": "A list of edges.",
"args": [
@@ -31458,6 +31589,57 @@
"deprecationReason": null
},
{
+ "name": "environment",
+ "description": "A single environment of the project",
+ "args": [
+ {
+ "name": "name",
+ "description": "Name of the environment",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Search query for environment name",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "states",
+ "description": "States of environments that should be included in result",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Environment",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "environments",
"description": "Environments of the project",
"args": [
@@ -35657,6 +35839,55 @@
},
{
"kind": "OBJECT",
+ "name": "PrometheusAlert",
+ "description": "The alert condition for Prometheus",
+ "fields": [
+ {
+ "name": "humanizedText",
+ "description": "The human-readable text of the alert condition",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the alert condition",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Query",
"description": null,
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 932135d443d..cadcacb7f48 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -64,6 +64,7 @@ Describes an alert from the project's Alert Management
| `createdAt` | Time | Timestamp the alert was created |
| `description` | String | Description of the alert |
| `details` | JSON | Alert details |
+| `detailsUrl` | String! | The URL of the alert detail page |
| `endedAt` | Time | Timestamp the alert ended |
| `eventCount` | Int | Number of events of this alert |
| `hosts` | String! => Array | List of hosts the alert came from |
@@ -71,6 +72,7 @@ Describes an alert from the project's Alert Management
| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
| `metricsDashboardUrl` | String | URL for metrics embed for the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
+| `prometheusAlert` | PrometheusAlert | The alert condition for Prometheus |
| `runbook` | String | Runbook for the alert as defined in alert details |
| `service` | String | Service the alert came from |
| `severity` | AlertManagementSeverity | Severity of the alert |
@@ -739,6 +741,7 @@ Describes where code is deployed for a project
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID of the environment |
+| `latestOpenedMostSevereAlert` | AlertManagementAlert | The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. |
| `metricsDashboard` | MetricsDashboard | Metrics dashboard schema for the environment |
| `name` | String! | Human-readable name of the environment |
| `state` | String! | State of the environment, for example: available/stopped |
@@ -1257,6 +1260,7 @@ Autogenerated return type of MarkAsSpamSnippet
| --- | ---- | ---------- |
| `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork |
| `author` | User | User who created this merge request |
+| `commitCount` | Int | Number of commits in the merge request |
| `createdAt` | Time! | Timestamp of when the merge request was created |
| `defaultMergeCommitMessage` | String | Default merge commit message of the merge request |
| `description` | String | Description of the merge request (Markdown rendered as HTML for caching) |
@@ -1602,6 +1606,7 @@ Information about pagination in a connection.
| `createdAt` | Time | Timestamp of the project creation |
| `description` | String | Short description of the project |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
+| `environment` | Environment | A single environment of the project |
| `forksCount` | Int! | Number of times the project has been forked |
| `fullPath` | ID! | Full path of the project |
| `grafanaIntegration` | GrafanaIntegration | Grafana integration details for the project |
@@ -1732,6 +1737,15 @@ Represents a Project Member
| `storageSize` | Float! | Storage size of the project |
| `wikiSize` | Float | Wiki size of the project |
+## PrometheusAlert
+
+The alert condition for Prometheus
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `humanizedText` | String! | The human-readable text of the alert condition |
+| `id` | ID! | ID of the alert condition |
+
## Release
Represents a release
diff --git a/doc/api/personal_access_tokens.md b/doc/api/personal_access_tokens.md
index 162ba88f727..517e26f3d85 100644
--- a/doc/api/personal_access_tokens.md
+++ b/doc/api/personal_access_tokens.md
@@ -4,7 +4,7 @@ You can read more about [personal access tokens](../user/profile/personal_access
## List personal access tokens
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22726) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227264) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
Get a list of personal access tokens.
@@ -60,3 +60,29 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
}
]
```
+
+## Revoke a personal access token
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
+
+Revoke a personal access token.
+
+```plaintext
+DELETE /personal_access_tokens/:id
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer/string | yes | ID of personal access token |
+
+NOTE: **Note:**
+Non-administrators can revoke their own tokens. Administrators can revoke tokens of any user.
+
+```shell
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/personal_access_tokens/<personal_access_token_id>"
+```
+
+### Responses
+
+- `204: No Content` if successfully revoked.
+- `400 Bad Request` if not revoked successfully.
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index 70f29d961e3..a0d871af127 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -6,7 +6,7 @@ NOTE: **Note:**
The former Vulnerabilities API was renamed to Vulnerability Findings API
and its documentation was moved to [a different location](vulnerability_findings.md).
This document now describes the new Vulnerabilities API that provides access to
-[Standalone Vulnerabilities](https://gitlab.com/groups/gitlab-org/-/epics/634).
+[Vulnerabilities](https://gitlab.com/groups/gitlab-org/-/epics/634).
CAUTION: **Caution:**
This API is in an alpha stage and considered unstable.
diff --git a/doc/api/vulnerability_findings.md b/doc/api/vulnerability_findings.md
index e21d903e474..96171f0229d 100644
--- a/doc/api/vulnerability_findings.md
+++ b/doc/api/vulnerability_findings.md
@@ -4,7 +4,7 @@
NOTE: **Note:**
This API resource is renamed from Vulnerabilities to Vulnerability Findings because the Vulnerabilities are reserved
-for serving the upcoming [Standalone Vulnerability objects](https://gitlab.com/gitlab-org/gitlab/-/issues/13561).
+for serving [Vulnerability objects](https://gitlab.com/gitlab-org/gitlab/-/issues/13561).
To fix any broken integrations with the former Vulnerabilities API, change the `vulnerabilities` URL part to be
`vulnerability_findings`.
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
index a8ae49543a6..1cfa698bfa5 100644
--- a/doc/ci/parent_child_pipelines.md
+++ b/doc/ci/parent_child_pipelines.md
@@ -43,8 +43,8 @@ Child pipelines work well with other GitLab CI/CD features:
- Since the parent pipeline in `.gitlab-ci.yml` and the child pipeline run as normal
pipelines, they can have their own behaviors and sequencing in relation to triggers.
-All of this will work with the [`include:`](yaml/README.md#include) feature so you can compose
-the child pipeline configuration.
+See the [`trigger:`](yaml/README.md#trigger) keyword documentation for full details on how to
+include the child pipeline configuration.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk).
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 1c778788827..c252f6425d0 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -360,10 +360,10 @@ Credit: [Avoid ableist language](https://developers.google.com/style/inclusive-d
Avoid terms that reflect negative cultural stereotypes and history. In most cases, you can replace terms such as `master` and `slave` with terms that are more precise and functional, such as `primary` and `secondary`.
-| Use | Avoid |
-|-----------------------|----------------------|
-| Primary / secondary | Master / slave |
-| Blacklist / whitelist | Allowlist / denylist |
+| Use | Avoid |
+|----------------------|-----------------------|
+| Primary / secondary | Master / slave |
+| Allowlist / denylist | Blacklist / whitelist |
For more information see the following [Internet Draft specification](https://tools.ietf.org/html/draft-knodel-terminology-02).
diff --git a/doc/operations/incident_management/img/incident_list.png b/doc/operations/incident_management/img/incident_list.png
new file mode 100644
index 00000000000..0498fec6c9c
--- /dev/null
+++ b/doc/operations/incident_management/img/incident_list.png
Binary files differ
diff --git a/doc/operations/incident_management/index.md b/doc/operations/incident_management/index.md
index 5db6c76a42b..a44d2cc0807 100644
--- a/doc/operations/incident_management/index.md
+++ b/doc/operations/incident_management/index.md
@@ -16,9 +16,7 @@ GitLab offers solutions for handling incidents in your applications and services
such as [setting up Prometheus alerts](#configure-prometheus-alerts),
[displaying metrics](#embed-metrics-in-incidents-and-issues), and sending notifications.
While no configuration is required to use the [manual features](#create-an-incident-manually)
-of incident management, both automation and [configuration](#configure-incidents-ultimate)
-of incident management are only available in
-[GitLab Ultimate and GitLab.com Gold](https://about.gitlab.com/pricing/).
+of incident management, some simple [configuration](#configure-incidents) is needed to automate incident creation.
For users with at least Developer [permissions](../../user/permissions.md), the
Incident Management list is available at **Operations > Incidents**
@@ -328,7 +326,7 @@ You can be alerted via a Slack message when a new alert has been received.
See the [Slack Notifications Service docs](../../user/project/integrations/slack.md) for information on how to set this up.
-## Configure incidents **(ULTIMATE)**
+## Configure incidents
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4925) in GitLab Ultimate 11.11.
@@ -366,7 +364,23 @@ sends these emails to [owners and maintainers](../../user/permissions.md) of the
These emails contain details of the alert, and a link for more information.
To send separate email notifications to users with
-[Developer permissions](../../user/permissions.md), see [Configure incidents](#configure-incidents-ultimate).
+[Developer permissions](../../user/permissions.md), see [Configure incidents](#configure-incidents).
+
+## Incident List
+
+Incidents in GitLab are aggregated in the Incident List, available at
+**Operations > Incidents**. This list displays all incidents in GitLab, with tabs
+to display open incidents, closed incidents, and all incidents:
+
+![Incident list](img/incident_list.png)
+
+The list displays the following attributes:
+
+- **Incident title**
+- **Date created** - in 'time ago' format.
+- **Assignees** - the avatar of the user assigned to the incident.
+- **Published** - Displays a green check mark (**{check-circle}**) if the incident is published
+ to a [Status Page](status_page.md).
## Create an incident manually
diff --git a/doc/operations/metrics/alerts.md b/doc/operations/metrics/alerts.md
index 6b5cbab8399..2ed8de9396a 100644
--- a/doc/operations/metrics/alerts.md
+++ b/doc/operations/metrics/alerts.md
@@ -14,7 +14,6 @@ your team when environment performance falls outside of the boundaries you set.
## Managed Prometheus instances
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](index.md#adding-custom-metrics), and GitLab 11.3 for [library metrics](../../user/project/integrations/prometheus_library/metrics.md).
-> - Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3.
For managed Prometheus instances using auto configuration, you can
[configure alerts for metrics](index.md#adding-custom-metrics) directly in the
@@ -32,6 +31,18 @@ For managed Prometheus instances using auto configuration, you can
To remove the alert, click back on the alert icon for the desired metric, and click **Delete**.
+### Link runbooks to alerts
+
+> - Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3.
+
+When creating alerts from the metrics dashboard for [managed Prometheus instances](#managed-prometheus-instances),
+you can also link a runbook. When the alert triggers, the
+[chart context menu](dashboards/index.md#chart-context-menu) on the metrics chart
+links to the runbook, making it easy for you to locate and access the correct runbook
+as soon as the alert fires:
+
+![Linked Runbook in charts](img/linked_runbooks_on_charts.png)
+
## External Prometheus instances
>- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9258) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.8.
diff --git a/doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png b/doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png
deleted file mode 100644
index 2d7cb923981..00000000000
--- a/doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png b/doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png
new file mode 100644
index 00000000000..1259917608b
--- /dev/null
+++ b/doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png
Binary files differ
diff --git a/doc/operations/metrics/dashboards/index.md b/doc/operations/metrics/dashboards/index.md
index 11d2dc45008..ffcb7dc92c6 100644
--- a/doc/operations/metrics/dashboards/index.md
+++ b/doc/operations/metrics/dashboards/index.md
@@ -136,7 +136,7 @@ You can take action related to a chart's data by clicking the
**{ellipsis_v}** **More actions** dropdown box above the upper right corner of
any chart on a dashboard:
-![Context Menu](img/panel_context_menu_v13_0.png)
+![Context Menu](img/panel_context_menu_v13_3.png)
The options are:
@@ -148,7 +148,10 @@ The options are:
feature, logs narrow down to the selected time range. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/122013) in GitLab 12.8.)
- **Download CSV** - Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
- [Copy link to chart](../embed.md#embedding-gitlab-managed-kubernetes-metrics)
-- [Alerts](../alerts.md)
+- **Alerts** - Display any [alerts](../alerts.md) configured for this metric.
+- **View Runbook** - Displays the runbook for an alert. For information about configuring
+ runbooks, read [Set up alerts for Prometheus metrics](../alerts.md).
+ ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211844) in GitLab 13.3.)
### Timeline zoom and URL sharing
diff --git a/doc/operations/metrics/img/linked_runbooks_on_charts.png b/doc/operations/metrics/img/linked_runbooks_on_charts.png
new file mode 100644
index 00000000000..335ba5dc172
--- /dev/null
+++ b/doc/operations/metrics/img/linked_runbooks_on_charts.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/standalone_vulnerability_page_v13_1.png b/doc/user/application_security/security_dashboard/img/vulnerability_page_v13_1.png
index 9cf95b197fe..9cf95b197fe 100644
--- a/doc/user/application_security/security_dashboard/img/standalone_vulnerability_page_v13_1.png
+++ b/doc/user/application_security/security_dashboard/img/vulnerability_page_v13_1.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index e4a3345d321..b8fcc513cb1 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -203,7 +203,7 @@ Clicking any vulnerability in the table takes you to its
[Vulnerability Details](../vulnerabilities) page to see more information on that vulnerability.
To create an issue associated with the vulnerability, click the **Create Issue** button.
-![Create an issue for the vulnerability](img/standalone_vulnerability_page_v13_1.png)
+![Create an issue for the vulnerability](img/vulnerability_page_v13_1.png)
Once you create the issue, the vulnerability list contains a link to the issue and an icon whose
color indicates the issue's status (green for open issues, blue for closed issues).
diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_download_patch_button_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_download_patch_button_v13_1.png
index b925c342a11..b925c342a11 100644
--- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_download_patch_button_v13_1.png
+++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_download_patch_button_v13_1.png
Binary files differ
diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_dropdown_v13_1.png
index 05ca74c3d5c..05ca74c3d5c 100644
--- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png
+++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_dropdown_v13_1.png
Binary files differ
diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_v13_1.png
index a3034a7db04..a3034a7db04 100644
--- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_merge_request_button_v13_1.png
+++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_merge_request_button_v13_1.png
Binary files differ
diff --git a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_v13_1.png b/doc/user/application_security/vulnerabilities/img/vulnerability_page_v13_1.png
index 30a7195e1ab..30a7195e1ab 100644
--- a/doc/user/application_security/vulnerabilities/img/standalone_vulnerability_page_v13_1.png
+++ b/doc/user/application_security/vulnerabilities/img/vulnerability_page_v13_1.png
Binary files differ
diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md
index d5cce6434d8..ffec4bf336d 100644
--- a/doc/user/application_security/vulnerabilities/index.md
+++ b/doc/user/application_security/vulnerabilities/index.md
@@ -5,16 +5,16 @@ 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/#designated-technical-writers
---
-# Standalone Vulnerability pages
+# Vulnerability Pages
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13561) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
Each security vulnerability in the [Security Dashboard](../security_dashboard/index.md#project-security-dashboard) has its own standalone
page.
-![Standalone vulnerability page](img/standalone_vulnerability_page_v13_1.png)
+![Vulnerability page](img/vulnerability_page_v13_1.png)
-On the standalone vulnerability page, you can interact with the vulnerability in
+On the vulnerability page, you can interact with the vulnerability in
several different ways:
- [Change the Vulnerability Status](#changing-vulnerability-status) - You can change the
@@ -57,7 +57,7 @@ generates for you. GitLab supports the following scanners:
When an automatic solution is available, the button in the header will show "Resolve with merge request":
-![Resolve with Merge Request button](img/standalone_vulnerability_page_merge_request_button_v13_1.png)
+![Resolve with Merge Request button](img/vulnerability_page_merge_request_button_v13_1.png)
Selecting the button will create a merge request with the automatic solution.
@@ -66,8 +66,8 @@ Selecting the button will create a merge request with the automatic solution.
To manually apply the patch that was generated by GitLab for a vulnerability, select the dropdown arrow on the "Resolve
with merge request" button, then select the "Download patch to resolve" option:
-![Resolve with Merge Request button dropdown](img/standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png)
+![Resolve with Merge Request button dropdown](img/vulnerability_page_merge_request_button_dropdown_v13_1.png)
This will change the button text to "Download patch to resolve". Click on it to download the patch:
-![Download patch button](img/standalone_vulnerability_page_download_patch_button_v13_1.png)
+![Download patch button](img/vulnerability_page_download_patch_button_v13_1.png)
diff --git a/doc/user/group/saml_sso/group_managed_accounts.md b/doc/user/group/saml_sso/group_managed_accounts.md
index 08455dc4725..126970ebbb6 100644
--- a/doc/user/group/saml_sso/group_managed_accounts.md
+++ b/doc/user/group/saml_sso/group_managed_accounts.md
@@ -7,8 +7,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Group Managed Accounts **(PREMIUM)**
-CAUTION: **Warning:**
-This is a [Closed Beta](https://about.gitlab.com/handbook/product/#closed-beta) feature.
+CAUTION: **Caution:**
+This [Closed Beta](https://about.gitlab.com/handbook/product/#closed-beta) feature is being re-evaluated in favor of a different
+[identity model](https://gitlab.com/gitlab-org/gitlab/-/issues/218631) that does not require separate accounts.
+We recommend that group administrators who haven't yet implemented this feature wait for
+the new solution.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/709) in GitLab 12.1.
> - It's deployed behind a feature flag, disabled by default.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index d8e8ab2fab7..800eb1d3359 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1301,6 +1301,58 @@ X-Gitlab-Event: Job Hook
Note that `commit.id` is the ID of the pipeline, not the ID of the commit.
+### Deployment events
+
+Triggered when deployment is finished/failed/canceled.
+
+**Request Header**:
+
+```plaintext
+X-Gitlab-Event: Deployment Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "deployment",
+ "status": "success",
+ "deployable_id": 796,
+ "deployable_url": "http://10.126.0.2:3000/root/test-deployment-webhooks/-/jobs/796",
+ "environment": "staging",
+ "project": {
+ "id": 30,
+ "name": "test-deployment-webhooks",
+ "description": "",
+ "web_url": "http://10.126.0.2:3000/root/test-deployment-webhooks",
+ "avatar_url": null,
+ "git_ssh_url": "ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git",
+ "git_http_url": "http://10.126.0.2:3000/root/test-deployment-webhooks.git",
+ "namespace": "Administrator",
+ "visibility_level": 0,
+ "path_with_namespace": "root/test-deployment-webhooks",
+ "default_branch": "master",
+ "ci_config_path": "",
+ "homepage": "http://10.126.0.2:3000/root/test-deployment-webhooks",
+ "url": "ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git",
+ "ssh_url": "ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git",
+ "http_url": "http://10.126.0.2:3000/root/test-deployment-webhooks.git"
+ },
+ "short_sha": "279484c0",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "email": "admin@example.com"
+ },
+ "user_url": "http://10.126.0.2:3000/root",
+ "commit_url": "http://10.126.0.2:3000/root/test-deployment-webhooks/-/commit/279484c09fbe69ededfced8c1bb6e6d24616b468",
+ "commit_title": "Add new file"
+}
+```
+
+Note that `deployable_id` is the ID of the CI job.
+
## Image URL rewriting
From GitLab 11.2, simple image references are rewritten to use an absolute URL
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index 4e7d460e7c7..371469a6ed6 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -206,13 +206,10 @@ viewed by browsing previous versions.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34382) in GitLab 13.3.
-You can change designs order with dragging design to the new position:
+You can change the order of designs by dragging them to a new position:
![Reorder designs](img/designs_reordering_v13_3.gif)
-NOTE: **Note:**
-You can reorder designs only on the latest version.
-
## Starting discussions on designs
When a design is uploaded, you can start a discussion by clicking on
diff --git a/doc/user/project/merge_requests/fail_fast_testing.md b/doc/user/project/merge_requests/fail_fast_testing.md
index 619a6d04577..60f81159394 100644
--- a/doc/user/project/merge_requests/fail_fast_testing.md
+++ b/doc/user/project/merge_requests/fail_fast_testing.md
@@ -45,8 +45,9 @@ This template requires:
- Use [Pipelines for Merge Requests](../../../ci/merge_request_pipelines/index.md#configuring-pipelines-for-merge-requests)
- [Pipelines for Merged Results](../../../ci/merge_request_pipelines/pipelines_for_merged_results/index.md#enable-pipelines-for-merged-results)
enabled in the project settings.
+- A Docker image with Ruby available. The template uses `image: ruby:2.6` by default, but you [can override](../../../ci/yaml/includes.md#overriding-external-template-values) this.
-## Configure Fast RSpec Failure
+## Configuring Fast RSpec Failure
We'll use the following plain RSpec configuration as a starting point. It installs all the
project gems and executes `rspec`, on merge request pipelines only.
@@ -69,6 +70,16 @@ include:
- template: Verify/FailFast.gitlab-ci.yml
```
+To customize the job, specific options may be set to override the template. For example, to override the default Docker image:
+
+```yaml
+include:
+ - template: Verify/FailFast.gitlab-ci.yml
+
+rspec-rails-modified-path-specs:
+ image: custom-docker-image-with-ruby
+```
+
### Example test loads
For illustrative purposes, let's say our Rails app spec suite consists of 100 specs per model for ten models.
diff --git a/doc/user/project/merge_requests/load_performance_testing.md b/doc/user/project/merge_requests/load_performance_testing.md
index 3239269109d..97f4f202ab3 100644
--- a/doc/user/project/merge_requests/load_performance_testing.md
+++ b/doc/user/project/merge_requests/load_performance_testing.md
@@ -141,7 +141,8 @@ For example, you can override the duration of the test with a CLI option:
GitLab only displays the key performance metrics in the MR widget if k6's results are saved
via [summary export](https://k6.io/docs/results-visualization/json#summary-export)
as a [Load Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsload_performance-premium).
-The latest Load Performance artifact available is always used.
+The latest Load Performance artifact available is always used, using the
+summary values from the test.
If [GitLab Pages](../pages/index.md) is enabled, you can view the report directly in your browser.
diff --git a/doc/user/project/merge_requests/test_coverage_visualization.md b/doc/user/project/merge_requests/test_coverage_visualization.md
index 793cedb0210..6751dde155c 100644
--- a/doc/user/project/merge_requests/test_coverage_visualization.md
+++ b/doc/user/project/merge_requests/test_coverage_visualization.md
@@ -54,6 +54,10 @@ from any job in any stage in the pipeline. The coverage will be displayed for ea
Hovering over the coverage bar will provide further information, such as the number
of times the line was checked by tests.
+NOTE: **Note:**
+The Cobertura XML parser currently does not support the `sources` element and ignores it. It is assumed that
+the `filename` of a `class` element contains the full path relative to the project root.
+
## Example test coverage configuration
The following [`gitlab-ci.yml`](../../../ci/yaml/README.md) example uses [Mocha](https://mochajs.org/)
diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb
index 6b0ff5e9395..8721d94d642 100644
--- a/lib/api/admin/ci/variables.rb
+++ b/lib/api/admin/ci/variables.rb
@@ -12,7 +12,7 @@ module API
namespace 'ci' do
namespace 'variables' do
desc 'Get instance-level variables' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
use :pagination
@@ -20,11 +20,11 @@ module API
get '/' do
variables = ::Ci::InstanceVariable.all
- present paginate(variables), with: Entities::Variable
+ present paginate(variables), with: Entities::Ci::Variable
end
desc 'Get a specific variable from a group' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
@@ -35,11 +35,11 @@ module API
break not_found!('InstanceVariable') unless variable
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
end
desc 'Create a new instance-level variable' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key,
@@ -69,14 +69,14 @@ module API
variable = ::Ci::InstanceVariable.new(variable_params)
if variable.save
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
end
desc 'Update an existing instance-variable' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
optional :key,
@@ -108,14 +108,14 @@ module API
variable_params = declared_params(include_missing: false).except(:key)
if variable.update(variable_params)
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
end
desc 'Delete an existing instance-level variable' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb
index 80ad8aa04dd..1afdb0ad34c 100644
--- a/lib/api/ci/pipeline_schedules.rb
+++ b/lib/api/ci/pipeline_schedules.rb
@@ -12,7 +12,7 @@ module API
end
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all pipeline schedules' do
- success Entities::PipelineSchedule
+ success Entities::Ci::PipelineSchedule
end
params do
use :pagination
@@ -25,22 +25,22 @@ module API
schedules = ::Ci::PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope])
.preload([:owner, :last_pipeline])
- present paginate(schedules), with: Entities::PipelineSchedule
+ present paginate(schedules), with: Entities::Ci::PipelineSchedule
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get a single pipeline schedule' do
- success Entities::PipelineScheduleDetails
+ success Entities::Ci::PipelineScheduleDetails
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end
get ':id/pipeline_schedules/:pipeline_schedule_id' do
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails
end
desc 'Create a new pipeline schedule' do
- success Entities::PipelineScheduleDetails
+ success Entities::Ci::PipelineScheduleDetails
end
params do
requires :description, type: String, desc: 'The description of pipeline schedule'
@@ -57,14 +57,14 @@ module API
.execute
if pipeline_schedule.persisted?
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails
else
render_validation_error!(pipeline_schedule)
end
end
desc 'Edit a pipeline schedule' do
- success Entities::PipelineScheduleDetails
+ success Entities::Ci::PipelineScheduleDetails
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
@@ -78,14 +78,14 @@ module API
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.update(declared_params(include_missing: false))
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails
else
render_validation_error!(pipeline_schedule)
end
end
desc 'Take ownership of a pipeline schedule' do
- success Entities::PipelineScheduleDetails
+ success Entities::Ci::PipelineScheduleDetails
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
@@ -94,14 +94,14 @@ module API
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.own!(current_user)
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ present pipeline_schedule, with: Entities::Ci::PipelineScheduleDetails
else
render_validation_error!(pipeline_schedule)
end
end
desc 'Delete a pipeline schedule' do
- success Entities::PipelineScheduleDetails
+ success Entities::Ci::PipelineScheduleDetails
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
@@ -132,7 +132,7 @@ module API
end
desc 'Create a new pipeline schedule variable' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
@@ -146,14 +146,14 @@ module API
variable_params = declared_params(include_missing: false)
variable = pipeline_schedule.variables.create(variable_params)
if variable.persisted?
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
end
desc 'Edit a pipeline schedule variable' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
@@ -165,14 +165,14 @@ module API
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule_variable.update(declared_params(include_missing: false))
- present pipeline_schedule_variable, with: Entities::Variable
+ present pipeline_schedule_variable, with: Entities::Ci::Variable
else
render_validation_error!(pipeline_schedule_variable)
end
end
desc 'Delete a pipeline schedule variable' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
@@ -182,7 +182,7 @@ module API
authorize! :admin_pipeline_schedule, pipeline_schedule
status :accepted
- present pipeline_schedule_variable.destroy, with: Entities::Variable
+ present pipeline_schedule_variable.destroy, with: Entities::Ci::Variable
end
end
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 4fb301f0260..bbbf3b683c5 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -13,7 +13,7 @@ module API
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
- success Entities::PipelineBasic
+ success Entities::Ci::PipelineBasic
end
params do
use :pagination
@@ -38,12 +38,12 @@ module API
authorize! :read_build, user_project
pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute
- present paginate(pipelines), with: Entities::PipelineBasic
+ present paginate(pipelines), with: Entities::Ci::PipelineBasic
end
desc 'Create a new pipeline' do
detail 'This feature was introduced in GitLab 8.14'
- success Entities::Pipeline
+ success Entities::Ci::Pipeline
end
params do
requires :ref, type: String, desc: 'Reference'
@@ -64,7 +64,7 @@ module API
.execute(:api, ignore_skip_ci: true, save_on_errors: false)
if new_pipeline.persisted?
- present new_pipeline, with: Entities::Pipeline
+ present new_pipeline, with: Entities::Ci::Pipeline
else
render_validation_error!(new_pipeline)
end
@@ -72,7 +72,7 @@ module API
desc 'Gets a the latest pipeline for the project branch' do
detail 'This feature was introduced in GitLab 12.3'
- success Entities::Pipeline
+ success Entities::Ci::Pipeline
end
params do
optional :ref, type: String, desc: 'branch ref of pipeline'
@@ -80,12 +80,12 @@ module API
get ':id/pipelines/latest' do
authorize! :read_pipeline, latest_pipeline
- present latest_pipeline, with: Entities::Pipeline
+ present latest_pipeline, with: Entities::Ci::Pipeline
end
desc 'Gets a specific pipeline for the project' do
detail 'This feature was introduced in GitLab 8.11'
- success Entities::Pipeline
+ success Entities::Ci::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
@@ -93,12 +93,12 @@ module API
get ':id/pipelines/:pipeline_id' do
authorize! :read_pipeline, pipeline
- present pipeline, with: Entities::Pipeline
+ present pipeline, with: Entities::Ci::Pipeline
end
desc 'Gets the variables for a given pipeline' do
detail 'This feature was introduced in GitLab 11.11'
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
@@ -106,7 +106,7 @@ module API
get ':id/pipelines/:pipeline_id/variables' do
authorize! :read_pipeline_variable, pipeline
- present pipeline.variables, with: Entities::Variable
+ present pipeline.variables, with: Entities::Ci::Variable
end
desc 'Gets the test report for a given pipeline' do
@@ -141,7 +141,7 @@ module API
desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
+ success Entities::Ci::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
@@ -151,12 +151,12 @@ module API
pipeline.retry_failed(current_user)
- present pipeline, with: Entities::Pipeline
+ present pipeline, with: Entities::Ci::Pipeline
end
desc 'Cancel all builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
+ success Entities::Ci::Pipeline
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
@@ -167,7 +167,7 @@ module API
pipeline.cancel_running
status 200
- present pipeline.reset, with: Entities::Pipeline
+ present pipeline.reset, with: Entities::Ci::Pipeline
end
end
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
index 2c156a71160..7bca72f8028 100644
--- a/lib/api/ci/runners.rb
+++ b/lib/api/ci/runners.rb
@@ -111,7 +111,7 @@ module API
end
desc 'List jobs running on a runner' do
- success Entities::JobBasicWithProject
+ success Entities::Ci::JobBasicWithProject
end
params do
requires :id, type: Integer, desc: 'The ID of the runner'
@@ -126,7 +126,7 @@ module API
jobs = ::Ci::RunnerJobsFinder.new(runner, params).execute
- present paginate(jobs), with: Entities::JobBasicWithProject
+ present paginate(jobs), with: Entities::Ci::JobBasicWithProject
end
end
diff --git a/lib/api/entities/bridge.rb b/lib/api/entities/bridge.rb
deleted file mode 100644
index 8f0ee69399a..00000000000
--- a/lib/api/entities/bridge.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class Bridge < Entities::JobBasic
- expose :downstream_pipeline, with: Entities::PipelineBasic
- end
- end
-end
diff --git a/lib/api/entities/ci/bridge.rb b/lib/api/entities/ci/bridge.rb
new file mode 100644
index 00000000000..502d97fff90
--- /dev/null
+++ b/lib/api/entities/ci/bridge.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class Bridge < JobBasic
+ expose :downstream_pipeline, with: ::API::Entities::Ci::PipelineBasic
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/job.rb b/lib/api/entities/ci/job.rb
new file mode 100644
index 00000000000..7fe1a802e24
--- /dev/null
+++ b/lib/api/entities/ci/job.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class Job < JobBasic
+ # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5)
+ expose :artifacts_file, using: ::API::Entities::Ci::JobArtifactFile, if: -> (job, opts) { job.artifacts? }
+ expose :job_artifacts, as: :artifacts, using: ::API::Entities::Ci::JobArtifact
+ expose :runner, with: ::API::Entities::Runner
+ expose :artifacts_expire_at
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/job_artifact.rb b/lib/api/entities/ci/job_artifact.rb
new file mode 100644
index 00000000000..9e504aee383
--- /dev/null
+++ b/lib/api/entities/ci/job_artifact.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class JobArtifact < Grape::Entity
+ expose :file_type, :size, :filename, :file_format
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/job_artifact_file.rb b/lib/api/entities/ci/job_artifact_file.rb
new file mode 100644
index 00000000000..418eb408ab6
--- /dev/null
+++ b/lib/api/entities/ci/job_artifact_file.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class JobArtifactFile < Grape::Entity
+ expose :filename
+ expose :cached_size, as: :size
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb
new file mode 100644
index 00000000000..a29788c7abf
--- /dev/null
+++ b/lib/api/entities/ci/job_basic.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class JobBasic < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
+ expose :created_at, :started_at, :finished_at
+ expose :duration
+ expose :user, with: ::API::Entities::User
+ expose :commit, with: ::API::Entities::Commit
+ expose :pipeline, with: ::API::Entities::Ci::PipelineBasic
+
+ expose :web_url do |job, _options|
+ Gitlab::Routing.url_helpers.project_job_url(job.project, job)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/job_basic_with_project.rb b/lib/api/entities/ci/job_basic_with_project.rb
new file mode 100644
index 00000000000..736e611e5b1
--- /dev/null
+++ b/lib/api/entities/ci/job_basic_with_project.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class JobBasicWithProject < Entities::Ci::JobBasic
+ expose :project, with: Entities::ProjectIdentity
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/pipeline.rb b/lib/api/entities/ci/pipeline.rb
new file mode 100644
index 00000000000..3dd3b9c9eff
--- /dev/null
+++ b/lib/api/entities/ci/pipeline.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class Pipeline < PipelineBasic
+ expose :before_sha, :tag, :yaml_errors
+
+ expose :user, with: Entities::UserBasic
+ expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
+ expose :duration
+ expose :coverage
+ expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
+ pipeline.detailed_status(options[:current_user])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/pipeline_basic.rb b/lib/api/entities/ci/pipeline_basic.rb
new file mode 100644
index 00000000000..dbb9b828757
--- /dev/null
+++ b/lib/api/entities/ci/pipeline_basic.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class PipelineBasic < Grape::Entity
+ expose :id, :sha, :ref, :status
+ expose :created_at, :updated_at
+
+ expose :web_url do |pipeline, _options|
+ Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/pipeline_schedule.rb b/lib/api/entities/ci/pipeline_schedule.rb
new file mode 100644
index 00000000000..f1596b7d285
--- /dev/null
+++ b/lib/api/entities/ci/pipeline_schedule.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class PipelineSchedule < Grape::Entity
+ expose :id
+ expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
+ expose :created_at, :updated_at
+ expose :owner, using: ::API::Entities::UserBasic
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/pipeline_schedule_details.rb b/lib/api/entities/ci/pipeline_schedule_details.rb
new file mode 100644
index 00000000000..b233728b95b
--- /dev/null
+++ b/lib/api/entities/ci/pipeline_schedule_details.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class PipelineScheduleDetails < PipelineSchedule
+ expose :last_pipeline, using: ::API::Entities::Ci::PipelineBasic
+ expose :variables, using: ::API::Entities::Ci::Variable
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/ci/variable.rb b/lib/api/entities/ci/variable.rb
new file mode 100644
index 00000000000..f4d5248245a
--- /dev/null
+++ b/lib/api/entities/ci/variable.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Ci
+ class Variable < Grape::Entity
+ expose :variable_type, :key, :value
+ expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
+ expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }
+ expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/commit_detail.rb b/lib/api/entities/commit_detail.rb
index 22424b38bb9..61238102e9d 100644
--- a/lib/api/entities/commit_detail.rb
+++ b/lib/api/entities/commit_detail.rb
@@ -9,7 +9,7 @@ module API
expose :last_pipeline do |commit, options|
pipeline = commit.last_pipeline if can_read_pipeline?
- ::API::Entities::PipelineBasic.represent(pipeline, options)
+ ::API::Entities::Ci::PipelineBasic.represent(pipeline, options)
end
private
diff --git a/lib/api/entities/deployment.rb b/lib/api/entities/deployment.rb
index 3a97d3e3c09..4e3a4c289d9 100644
--- a/lib/api/entities/deployment.rb
+++ b/lib/api/entities/deployment.rb
@@ -6,7 +6,7 @@ module API
expose :id, :iid, :ref, :sha, :created_at, :updated_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
- expose :deployable, using: Entities::Job
+ expose :deployable, using: Entities::Ci::Job
expose :status
end
end
diff --git a/lib/api/entities/job.rb b/lib/api/entities/job.rb
deleted file mode 100644
index cbee8794007..00000000000
--- a/lib/api/entities/job.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class Job < Entities::JobBasic
- # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5)
- expose :artifacts_file, using: Entities::JobArtifactFile, if: -> (job, opts) { job.artifacts? }
- expose :job_artifacts, as: :artifacts, using: Entities::JobArtifact
- expose :runner, with: Entities::Runner
- expose :artifacts_expire_at
- end
- end
-end
diff --git a/lib/api/entities/job_artifact.rb b/lib/api/entities/job_artifact.rb
deleted file mode 100644
index 94dbdb38fee..00000000000
--- a/lib/api/entities/job_artifact.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class JobArtifact < Grape::Entity
- expose :file_type, :size, :filename, :file_format
- end
- end
-end
diff --git a/lib/api/entities/job_artifact_file.rb b/lib/api/entities/job_artifact_file.rb
deleted file mode 100644
index fa2851a7f0e..00000000000
--- a/lib/api/entities/job_artifact_file.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class JobArtifactFile < Grape::Entity
- expose :filename
- expose :cached_size, as: :size
- end
- end
-end
diff --git a/lib/api/entities/job_basic.rb b/lib/api/entities/job_basic.rb
deleted file mode 100644
index a8541039934..00000000000
--- a/lib/api/entities/job_basic.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class JobBasic < Grape::Entity
- expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
- expose :created_at, :started_at, :finished_at
- expose :duration
- expose :user, with: Entities::User
- expose :commit, with: Entities::Commit
- expose :pipeline, with: Entities::PipelineBasic
-
- expose :web_url do |job, _options|
- Gitlab::Routing.url_helpers.project_job_url(job.project, job)
- end
- end
- end
-end
diff --git a/lib/api/entities/job_basic_with_project.rb b/lib/api/entities/job_basic_with_project.rb
deleted file mode 100644
index 09387e045ec..00000000000
--- a/lib/api/entities/job_basic_with_project.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class JobBasicWithProject < Entities::JobBasic
- expose :project, with: Entities::ProjectIdentity
- end
- end
-end
diff --git a/lib/api/entities/job_request/dependency.rb b/lib/api/entities/job_request/dependency.rb
index 64d779f6575..7d6ec832ba1 100644
--- a/lib/api/entities/job_request/dependency.rb
+++ b/lib/api/entities/job_request/dependency.rb
@@ -5,7 +5,7 @@ module API
module JobRequest
class Dependency < Grape::Entity
expose :id, :name, :token
- expose :artifacts_file, using: Entities::JobArtifactFile, if: ->(job, _) { job.artifacts? }
+ expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? }
end
end
end
diff --git a/lib/api/entities/merge_request.rb b/lib/api/entities/merge_request.rb
index 7fc76a4071e..05ae041c7a9 100644
--- a/lib/api/entities/merge_request.rb
+++ b/lib/api/entities/merge_request.rb
@@ -23,11 +23,11 @@ module API
merge_request.metrics&.first_deployed_to_production_at
end
- expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ expose :pipeline, using: Entities::Ci::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
merge_request.metrics&.pipeline
end
- expose :head_pipeline, using: 'API::Entities::Pipeline', if: -> (_, options) do
+ expose :head_pipeline, using: '::API::Entities::Ci::Pipeline', if: -> (_, options) do
Ability.allowed?(options[:current_user], :read_pipeline, options[:project])
end
diff --git a/lib/api/entities/package/pipeline.rb b/lib/api/entities/package/pipeline.rb
index e91a12e47fa..0aa888e30ee 100644
--- a/lib/api/entities/package/pipeline.rb
+++ b/lib/api/entities/package/pipeline.rb
@@ -3,7 +3,7 @@
module API
module Entities
class Package < Grape::Entity
- class Pipeline < ::API::Entities::PipelineBasic
+ class Pipeline < ::API::Entities::Ci::PipelineBasic
expose :user, using: ::API::Entities::UserBasic
end
end
diff --git a/lib/api/entities/pipeline.rb b/lib/api/entities/pipeline.rb
deleted file mode 100644
index 778efbe4bcc..00000000000
--- a/lib/api/entities/pipeline.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class Pipeline < Entities::PipelineBasic
- expose :before_sha, :tag, :yaml_errors
-
- expose :user, with: Entities::UserBasic
- expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
- expose :duration
- expose :coverage
- expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
- pipeline.detailed_status(options[:current_user])
- end
- end
- end
-end
diff --git a/lib/api/entities/pipeline_basic.rb b/lib/api/entities/pipeline_basic.rb
deleted file mode 100644
index 359f6a447ab..00000000000
--- a/lib/api/entities/pipeline_basic.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class PipelineBasic < Grape::Entity
- expose :id, :sha, :ref, :status
- expose :created_at, :updated_at
-
- expose :web_url do |pipeline, _options|
- Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
- end
- end
- end
-end
diff --git a/lib/api/entities/pipeline_schedule.rb b/lib/api/entities/pipeline_schedule.rb
deleted file mode 100644
index a72fe3f3141..00000000000
--- a/lib/api/entities/pipeline_schedule.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class PipelineSchedule < Grape::Entity
- expose :id
- expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
- expose :created_at, :updated_at
- expose :owner, using: Entities::UserBasic
- end
- end
-end
diff --git a/lib/api/entities/pipeline_schedule_details.rb b/lib/api/entities/pipeline_schedule_details.rb
deleted file mode 100644
index 5e54489a0f9..00000000000
--- a/lib/api/entities/pipeline_schedule_details.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class PipelineScheduleDetails < Entities::PipelineSchedule
- expose :last_pipeline, using: Entities::PipelineBasic
- expose :variables, using: Entities::Variable
- end
- end
-end
diff --git a/lib/api/entities/variable.rb b/lib/api/entities/variable.rb
deleted file mode 100644
index 6705df30b2e..00000000000
--- a/lib/api/entities/variable.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class Variable < Grape::Entity
- expose :variable_type, :key, :value
- expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
- expose :masked?, as: :masked, if: -> (entity, _) { entity.respond_to?(:masked?) }
- expose :environment_scope, if: -> (entity, _) { entity.respond_to?(:environment_scope) }
- end
- end
-end
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index b5ff151f07d..e7b8cd10197 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -13,18 +13,18 @@ module API
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get group-level variables' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
use :pagination
end
get ':id/variables' do
variables = user_group.variables
- present paginate(variables), with: Entities::Variable
+ present paginate(variables), with: Entities::Ci::Variable
end
desc 'Get a specific variable from a group' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
@@ -36,12 +36,12 @@ module API
break not_found!('GroupVariable') unless variable
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Create a new variable in a group' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
@@ -58,14 +58,14 @@ module API
).execute
if variable.valid?
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
end
desc 'Update an existing variable from a group' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
optional :key, type: String, desc: 'The key of the variable'
@@ -83,7 +83,7 @@ module API
).execute
if variable.valid?
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
@@ -93,7 +93,7 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
desc 'Delete an existing variable from a group' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 61c279a76e9..bc7bc956580 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -94,7 +94,7 @@ module API
end
desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Job
+ success ::API::Entities::Ci::Job
end
params do
requires :job_id, type: Integer, desc: 'The ID of a job'
@@ -109,7 +109,7 @@ module API
build.keep_artifacts!
status 200
- present build, with: Entities::Job
+ present build, with: ::API::Entities::Ci::Job
end
desc 'Delete the artifacts files from a job' do
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 9fab722b72e..084c146abe7 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -30,7 +30,7 @@ module API
end
desc 'Get a projects jobs' do
- success Entities::Job
+ success Entities::Ci::Job
end
params do
use :optional_scope
@@ -44,12 +44,12 @@ module API
builds = filter_builds(builds, params[:scope])
builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, pipeline: :project)
- present paginate(builds), with: Entities::Job
+ present paginate(builds), with: Entities::Ci::Job
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get pipeline jobs' do
- success Entities::Job
+ success Entities::Ci::Job
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
@@ -66,12 +66,12 @@ module API
builds = filter_builds(builds, params[:scope])
builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
- present paginate(builds), with: Entities::Job
+ present paginate(builds), with: Entities::Ci::Job
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get pipeline bridge jobs' do
- success Entities::Bridge
+ success ::API::Entities::Ci::Bridge
end
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
@@ -92,12 +92,12 @@ module API
project: [:namespace]
)
- present paginate(bridges), with: Entities::Bridge
+ present paginate(bridges), with: ::API::Entities::Ci::Bridge
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get a specific job of a project' do
- success Entities::Job
+ success Entities::Ci::Job
end
params do
requires :job_id, type: Integer, desc: 'The ID of a job'
@@ -107,7 +107,7 @@ module API
build = find_build!(params[:job_id])
- present build, with: Entities::Job
+ present build, with: Entities::Ci::Job
end
# TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace
@@ -131,7 +131,7 @@ module API
end
desc 'Cancel a specific job of a project' do
- success Entities::Job
+ success Entities::Ci::Job
end
params do
requires :job_id, type: Integer, desc: 'The ID of a job'
@@ -144,11 +144,11 @@ module API
build.cancel
- present build, with: Entities::Job
+ present build, with: Entities::Ci::Job
end
desc 'Retry a specific build of a project' do
- success Entities::Job
+ success Entities::Ci::Job
end
params do
requires :job_id, type: Integer, desc: 'The ID of a build'
@@ -162,11 +162,11 @@ module API
build = ::Ci::Build.retry(build, current_user)
- present build, with: Entities::Job
+ present build, with: Entities::Ci::Job
end
desc 'Erase job (remove artifacts and the trace)' do
- success Entities::Job
+ success Entities::Ci::Job
end
params do
requires :job_id, type: Integer, desc: 'The ID of a build'
@@ -179,11 +179,11 @@ module API
break forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
- present build, with: Entities::Job
+ present build, with: Entities::Ci::Job
end
desc 'Trigger a actionable job (manual, delayed, etc)' do
- success Entities::Job
+ success Entities::Ci::Job
detail 'This feature was added in GitLab 8.11'
end
params do
@@ -200,7 +200,7 @@ module API
build.play(current_user)
status 200
- present build, with: Entities::Job
+ present build, with: Entities::Ci::Job
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 53a7a2498a6..6f25df720c4 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -352,16 +352,16 @@ module API
end
desc 'Get the merge request pipelines' do
- success Entities::PipelineBasic
+ success Entities::Ci::PipelineBasic
end
get ':id/merge_requests/:merge_request_iid/pipelines' do
pipelines = merge_request_pipelines_with_access
- present paginate(pipelines), with: Entities::PipelineBasic
+ present paginate(pipelines), with: Entities::Ci::PipelineBasic
end
desc 'Create a pipeline for merge request' do
- success Entities::Pipeline
+ success ::API::Entities::Ci::Pipeline
end
post ':id/merge_requests/:merge_request_iid/pipelines' do
pipeline = ::MergeRequests::CreatePipelineService
@@ -372,7 +372,7 @@ module API
not_allowed!
elsif pipeline.persisted?
status :ok
- present pipeline, with: Entities::Pipeline
+ present pipeline, with: ::API::Entities::Ci::Pipeline
else
render_validation_error!(pipeline)
end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index de67a149274..f398bbf3e32 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -11,7 +11,7 @@ module API
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Trigger a GitLab project pipeline' do
- success Entities::Pipeline
+ success Entities::Ci::Pipeline
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false
@@ -38,7 +38,7 @@ module API
if result[:http_status]
render_api_error!(result[:message], result[:http_status])
else
- present result[:pipeline], with: Entities::Pipeline
+ present result[:pipeline], with: Entities::Ci::Pipeline
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 50d137ec7c1..6f449fd060a 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -30,18 +30,18 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get project variables' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
use :pagination
end
get ':id/variables' do
variables = user_project.variables
- present paginate(variables), with: Entities::Variable
+ present paginate(variables), with: Entities::Ci::Variable
end
desc 'Get a specific variable from a project' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
@@ -51,12 +51,12 @@ module API
variable = find_variable(params)
not_found!('Variable') unless variable
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Create a new variable in a project' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
@@ -73,14 +73,14 @@ module API
variable = user_project.variables.create(variable_params)
if variable.valid?
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
end
desc 'Update an existing variable from a project' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
optional :key, type: String, desc: 'The key of the variable'
@@ -100,7 +100,7 @@ module API
variable_params = filter_variable_parameters(variable_params)
if variable.update(variable_params)
- present variable, with: Entities::Variable
+ present variable, with: Entities::Ci::Variable
else
render_validation_error!(variable)
end
@@ -108,7 +108,7 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
desc 'Delete an existing variable from a project' do
- success Entities::Variable
+ success Entities::Ci::Variable
end
params do
requires :key, type: String, desc: 'The key of the variable'
diff --git a/lib/gitlab/ci/parsers/coverage/cobertura.rb b/lib/gitlab/ci/parsers/coverage/cobertura.rb
index 006d5097148..934c797580c 100644
--- a/lib/gitlab/ci/parsers/coverage/cobertura.rb
+++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb
@@ -28,6 +28,8 @@ module Gitlab
end
def parse_node(key, value, coverage_report)
+ return if key == 'sources'
+
if key == 'class'
Array.wrap(value).each do |item|
parse_class(item, coverage_report)
diff --git a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml
index b437ddbd734..4a9849c85c9 100644
--- a/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Load-Performance-Testing.gitlab-ci.yml
@@ -5,7 +5,7 @@ load_performance:
variables:
DOCKER_TLS_CERTDIR: ""
K6_IMAGE: loadimpact/k6
- K6_VERSION: 0.26.2
+ K6_VERSION: 0.27.0
K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js
K6_OPTIONS: ''
services:
diff --git a/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml
index 77a1b57d92f..584e6966180 100644
--- a/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/FailFast.gitlab-ci.yml
@@ -1,4 +1,5 @@
rspec-rails-modified-path-specs:
+ image: ruby:2.6
stage: .pre
rules:
- if: $CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"
diff --git a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml
index d39bd234020..f964b3b2caf 100644
--- a/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Load-Performance-Testing.gitlab-ci.yml
@@ -11,7 +11,7 @@ load_performance:
image: docker:git
variables:
K6_IMAGE: loadimpact/k6
- K6_VERSION: 0.26.2
+ K6_VERSION: 0.27.0
K6_TEST_FILE: github.com/loadimpact/k6/samples/http_get.js
K6_OPTIONS: ''
services:
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index 346a2f9a461..e53ac00e77f 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -22,7 +22,7 @@ module Gitlab
return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql])
current_transaction.observe(:gitlab_sql_duration_seconds, event.duration / 1000.0) do
- buckets [0.05, 0.1]
+ buckets [0.05, 0.1, 0.25]
end
increment_db_counters(payload)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e6dfda7dec6..38120b51b0f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14993,6 +14993,27 @@ msgstr ""
msgid "MergeConflict|origin//their changes"
msgstr ""
+msgid "MergeRequestAnalytics|Assignees"
+msgstr ""
+
+msgid "MergeRequestAnalytics|Date Merged"
+msgstr ""
+
+msgid "MergeRequestAnalytics|Line changes"
+msgstr ""
+
+msgid "MergeRequestAnalytics|Merge Request"
+msgstr ""
+
+msgid "MergeRequestAnalytics|Milestone"
+msgstr ""
+
+msgid "MergeRequestAnalytics|Pipelines"
+msgstr ""
+
+msgid "MergeRequestAnalytics|Time to merge"
+msgstr ""
+
msgid "MergeRequestDiffs|Commenting on lines %{selectStart}start%{selectEnd} to %{end}"
msgstr ""
@@ -19432,9 +19453,18 @@ msgstr ""
msgid "PrometheusAlerts|Threshold"
msgstr ""
+msgid "PrometheusAlerts|exceeded"
+msgstr ""
+
msgid "PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks"
msgstr ""
+msgid "PrometheusAlerts|is equal to"
+msgstr ""
+
+msgid "PrometheusAlerts|is less than"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -24491,6 +24521,9 @@ msgstr ""
msgid "There is no data available. Please change your selection."
msgstr ""
+msgid "There is no table data available."
+msgstr ""
+
msgid "There is too much data to calculate. Please change your selection."
msgstr ""
@@ -24659,6 +24692,9 @@ msgstr ""
msgid "There was an error while fetching the chart data."
msgstr ""
+msgid "There was an error while fetching the table data."
+msgstr ""
+
msgid "There was an error while fetching value stream analytics data."
msgstr ""
diff --git a/package.json b/package.json
index cb19b90d0fa..5f70378f735 100644
--- a/package.json
+++ b/package.json
@@ -80,6 +80,7 @@
"deckar01-task_list": "^2.3.1",
"diff": "^3.4.0",
"document-register-element": "1.14.3",
+ "dompurify": "^2.0.11",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^7.0.3",
@@ -123,7 +124,6 @@
"prosemirror-model": "^1.6.4",
"raphael": "^2.2.7",
"raw-loader": "^4.0.0",
- "sanitize-html": "^1.22.0",
"select2": "3.5.2-browserify",
"smooshpack": "^0.0.62",
"sortablejs": "^1.10.2",
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 6fe5c9e0ff9..af6e88f73b1 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -43,6 +43,21 @@ FactoryBot.define do
state_id { MergeRequest.available_states[:merged] }
end
+ trait :with_merged_metrics do
+ merged
+
+ transient do
+ merged_by { author }
+ end
+
+ after(:build) do |merge_request, evaluator|
+ metrics = merge_request.build_metrics
+ metrics.merged_at = 1.week.ago
+ metrics.merged_by = evaluator.merged_by
+ metrics.pipeline = create(:ci_empty_pipeline)
+ end
+ end
+
trait :merged_target do
source_branch { "merged-target" }
target_branch { "improve/awesome" }
diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json
index f42d701834a..d1274bea817 100644
--- a/spec/fixtures/api/schemas/environment.json
+++ b/spec/fixtures/api/schemas/environment.json
@@ -33,6 +33,7 @@
"updated_at": { "type": "string", "format": "date-time" },
"auto_stop_at": { "type": "string", "format": "date-time" },
"can_stop": { "type": "boolean" },
+ "has_opened_alert": { "type": "boolean" },
"cluster_type": { "type": "types/nullable_string.json" },
"terminal_path": { "type": "types/nullable_string.json" },
"last_deployment": {
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index ad398d6ccd6..4e35243f484 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -1,6 +1,6 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
-import { GlDeprecatedButton, GlFormCombobox } from '@gitlab/ui';
+import { GlButton, GlFormCombobox } from '@gitlab/ui';
import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import createStore from '~/ci_variable_list/store';
@@ -29,14 +29,14 @@ describe('Ci variable modal', () => {
};
const findModal = () => wrapper.find(ModalStub);
- const addOrUpdateButton = index =>
+ const findAddorUpdateButton = () =>
findModal()
- .findAll(GlDeprecatedButton)
- .at(index);
+ .findAll(GlButton)
+ .wrappers.find(button => button.props('variant') === 'success');
const deleteVariableButton = () =>
findModal()
- .findAll(GlDeprecatedButton)
- .at(1);
+ .findAll(GlButton)
+ .wrappers.find(button => button.props('variant') === 'danger');
afterEach(() => {
wrapper.destroy();
@@ -69,7 +69,7 @@ describe('Ci variable modal', () => {
});
it('button is disabled when no key/value pair are present', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
});
});
@@ -82,11 +82,11 @@ describe('Ci variable modal', () => {
});
it('button is enabled when key/value pair are present', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
});
it('Add variable button dispatches addVariable action', () => {
- addOrUpdateButton(1).vm.$emit('click');
+ findAddorUpdateButton().vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('addVariable');
});
@@ -152,11 +152,11 @@ describe('Ci variable modal', () => {
});
it('button text is Update variable when updating', () => {
- expect(addOrUpdateButton(2).text()).toBe('Update variable');
+ expect(findAddorUpdateButton().text()).toBe('Update variable');
});
it('Update variable button dispatches updateVariable with correct variable', () => {
- addOrUpdateButton(2).vm.$emit('click');
+ findAddorUpdateButton().vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('updateVariable');
});
@@ -189,7 +189,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
});
it('shows the correct error text', () => {
@@ -213,7 +213,7 @@ describe('Ci variable modal', () => {
});
it('does not disable the submit button', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
});
});
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index ec42df0b0c3..aabafaa9154 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -7,11 +7,13 @@ import {
GlPagination,
GlSearchBoxByType,
GlTab,
+ GlTabs,
+ GlBadge,
} from '@gitlab/ui';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { I18N, INCIDENT_STATE_TABS } from '~/incidents/constants';
+import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
import mockIncidents from '../mocks/incidents.json';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -24,6 +26,11 @@ describe('Incidents List', () => {
let wrapper;
const newIssuePath = 'namespace/project/-/issues/new';
const incidentTemplateName = 'incident';
+ const incidentsCount = {
+ opened: 14,
+ closed: 1,
+ all: 16,
+ };
const findTable = () => wrapper.find(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr');
@@ -38,8 +45,10 @@ describe('Incidents List', () => {
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findPagination = () => wrapper.find(GlPagination);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
+ const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
+ const findStatusTabs = () => wrapper.find(GlTabs);
- function mountComponent({ data = { incidents: [] }, loading = false }) {
+ function mountComponent({ data = { incidents: [], incidentsCount: {} }, loading = false }) {
wrapper = mount(IncidentsList, {
data() {
return data;
@@ -83,7 +92,7 @@ describe('Incidents List', () => {
it('shows empty state', () => {
mountComponent({
- data: { incidents: { list: [] } },
+ data: { incidents: { list: [] }, incidentsCount: {} },
loading: false,
});
expect(findTable().text()).toContain(I18N.noIncidents);
@@ -91,7 +100,7 @@ describe('Incidents List', () => {
it('shows error state', () => {
mountComponent({
- data: { incidents: { list: [] }, errored: true },
+ data: { incidents: { list: [] }, incidentsCount: { all: 0 }, errored: true },
loading: false,
});
expect(findTable().text()).toContain(I18N.noIncidents);
@@ -101,7 +110,7 @@ describe('Incidents List', () => {
describe('Incident Management list', () => {
beforeEach(() => {
mountComponent({
- data: { incidents: { list: mockIncidents } },
+ data: { incidents: { list: mockIncidents }, incidentsCount },
loading: false,
});
});
@@ -153,7 +162,7 @@ describe('Incidents List', () => {
describe('Create Incident', () => {
beforeEach(() => {
mountComponent({
- data: { incidents: { list: [] } },
+ data: { incidents: { list: [] }, incidentsCount: {} },
loading: false,
});
});
@@ -178,6 +187,7 @@ describe('Incidents List', () => {
list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
+ incidentsCount,
errored: false,
},
loading: false,
@@ -240,6 +250,7 @@ describe('Incidents List', () => {
list: [...mockIncidents, ...mockIncidents, ...mockIncidents],
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
+ incidentsCount,
errored: false,
},
loading: false,
@@ -252,6 +263,7 @@ describe('Incidents List', () => {
});
it('returns `null` when currentPage is already last page', () => {
+ findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull();
@@ -267,6 +279,7 @@ describe('Incidents List', () => {
list: mockIncidents,
pageInfo: { hasNextPage: true, hasPreviousPage: true },
},
+ incidentsCount,
errored: false,
},
loading: false,
@@ -286,10 +299,10 @@ describe('Incidents List', () => {
});
});
- describe('State Filter Tabs', () => {
+ describe('Status Filter Tabs', () => {
beforeEach(() => {
mountComponent({
- data: { incidents: mockIncidents },
+ data: { incidents: mockIncidents, incidentsCount },
loading: false,
stubs: {
GlTab: true,
@@ -301,7 +314,18 @@ describe('Incidents List', () => {
const tabs = findStatusFilterTabs().wrappers;
tabs.forEach((tab, i) => {
- expect(tab.attributes('data-testid')).toContain(INCIDENT_STATE_TABS[i].state);
+ expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
+ });
+ });
+
+ it('should display filter tabs with alerts count badge for each status', () => {
+ const tabs = findStatusFilterTabs().wrappers;
+ const badges = findStatusFilterBadge();
+
+ tabs.forEach((tab, i) => {
+ const status = INCIDENT_STATUS_TABS[i].status.toLowerCase();
+ expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
+ expect(badges.at(i).text()).toContain(incidentsCount[status]);
});
});
});
@@ -310,7 +334,7 @@ describe('Incidents List', () => {
describe('sorting the incident list by column', () => {
beforeEach(() => {
mountComponent({
- data: { incidents: mockIncidents },
+ data: { incidents: mockIncidents, incidentsCount },
loading: false,
});
});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 87e6d2724f6..d9866a94ffe 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -639,3 +639,17 @@ describe('dateFromParams', () => {
expect(date.getDate()).toBe(expectedDate.getDate());
});
});
+
+describe('differenceInSeconds', () => {
+ const startDateTime = new Date('2019-07-17T00:00:00.000Z');
+
+ it.each`
+ startDate | endDate | expected
+ ${startDateTime} | ${new Date('2019-07-17T00:00:00.000Z')} | ${0}
+ ${startDateTime} | ${new Date('2019-07-17T12:00:00.000Z')} | ${43200}
+ ${startDateTime} | ${new Date('2019-07-18T00:00:00.000Z')} | ${86400}
+ ${new Date('2019-07-18T00:00:00.000Z')} | ${startDateTime} | ${-86400}
+ `('returns $expected for $endDate - $startDate', ({ startDate, endDate, expected }) => {
+ expect(datetimeUtility.differenceInSeconds(startDate, endDate)).toBe(expected);
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
new file mode 100644
index 00000000000..a886715ce4b
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
@@ -0,0 +1,114 @@
+export default [
+ [
+ 'protocol-based JS injection: simple, no spaces',
+ {
+ input: `<a href="javascript:alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: simple, spaces before',
+ {
+ input: `<a href="javascript :alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: simple, spaces after',
+ {
+ input: `<a href="javascript: alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: simple, spaces before and after',
+ {
+ input: `<a href="javascript : alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: preceding colon',
+ {
+ input: `<a href=":javascript:alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: UTF-8 encoding',
+ {
+ input: '<a href="javascript&#58;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: long UTF-8 encoding',
+ {
+ input: '<a href="javascript&#0058;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: long UTF-8 encoding without semicolons',
+ {
+ input:
+ '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: hex encoding',
+ {
+ input: '<a href="javascript&#x3A;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: long hex encoding',
+ {
+ input: '<a href="javascript&#x003A;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: hex encoding without semicolons',
+ {
+ input:
+ '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: null char',
+ {
+ input: '<a href=java\u0000script:alert("XSS")>foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: invalid URL char',
+ { input: '<img src=javascript:alert("XSS")>', output: '<img>' },
+ ],
+ [
+ 'protocol-based JS injection: Unicode',
+ {
+ input: `<a href="\u0001java\u0003script:alert('XSS')">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: spaces and entities',
+ {
+ input: `<a href=" &#14; javascript:alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'img on error',
+ {
+ input: '<img src="x" onerror="alert(document.domain)" />',
+ output: '<img src="x">',
+ },
+ ],
+ ['style tags are removed', { input: '<style>.foo {}</style> Foo', output: 'Foo' }],
+];
diff --git a/spec/frontend/notebook/cells/output/html_sanitize_tests.js b/spec/frontend/notebook/cells/output/html_sanitize_tests.js
deleted file mode 100644
index 74c48f04367..00000000000
--- a/spec/frontend/notebook/cells/output/html_sanitize_tests.js
+++ /dev/null
@@ -1,68 +0,0 @@
-export default {
- 'protocol-based JS injection: simple, no spaces': {
- input: '<a href="javascript:alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: simple, spaces before': {
- input: '<a href="javascript :alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: simple, spaces after': {
- input: '<a href="javascript: alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: simple, spaces before and after': {
- input: '<a href="javascript : alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: preceding colon': {
- input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: UTF-8 encoding': {
- input: '<a href="javascript&#58;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: long UTF-8 encoding': {
- input: '<a href="javascript&#0058;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: long UTF-8 encoding without semicolons': {
- input:
- '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: hex encoding': {
- input: '<a href="javascript&#x3A;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: long hex encoding': {
- input: '<a href="javascript&#x003A;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: hex encoding without semicolons': {
- input:
- '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: null char': {
- input: '<a href=java\0script:alert("XSS")>foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: invalid URL char': {
- input: '<img src=javascript:alert("XSS")>',
- output: '<img>',
- },
- 'protocol-based JS injection: Unicode': {
- input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: spaces and entities': {
- input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'img on error': {
- input: '<img src="x" onerror="alert(document.domain)" />',
- output: '<img src="x">',
- },
-};
diff --git a/spec/frontend/notebook/cells/output/html_spec.js b/spec/frontend/notebook/cells/output/html_spec.js
index 3ee404fb187..48d62d74a50 100644
--- a/spec/frontend/notebook/cells/output/html_spec.js
+++ b/spec/frontend/notebook/cells/output/html_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import htmlOutput from '~/notebook/cells/output/html.vue';
-import sanitizeTests from './html_sanitize_tests';
+import sanitizeTests from './html_sanitize_fixtures';
describe('html output cell', () => {
function createComponent(rawCode) {
@@ -15,17 +15,12 @@ describe('html output cell', () => {
}).$mount();
}
- describe('sanitizes output', () => {
- Object.keys(sanitizeTests).forEach(key => {
- it(key, () => {
- const test = sanitizeTests[key];
- const vm = createComponent(test.input);
- const outputEl = [...vm.$el.querySelectorAll('div')].pop();
+ it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => {
+ const vm = createComponent(input);
+ const outputEl = [...vm.$el.querySelectorAll('div')].pop();
- expect(outputEl.innerHTML).toEqual(test.output);
+ expect(outputEl.innerHTML).toEqual(output);
- vm.$destroy();
- });
- });
+ vm.$destroy();
});
});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 2b1aa5317c5..b9a2dfb8f34 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -34,7 +34,7 @@ describe('Output component', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
- it('renders promot', () => {
+ it('renders prompt', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
});
});
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
index 95eb5abd0cf..307976d4124 100644
--- a/spec/frontend/packages/details/store/getters_spec.js
+++ b/spec/frontend/packages/details/store/getters_spec.js
@@ -13,6 +13,8 @@ import {
nugetSetupCommand,
pypiPipCommand,
pypiSetupCommand,
+ composerRegistryInclude,
+ composerPackageInclude,
} from '~/packages/details/store/getters';
import {
conanPackage,
@@ -68,6 +70,10 @@ describe('Getters PackageDetails Store', () => {
const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
const pypiPipCommandStr = `pip install ${pypiPackage.name} --index-url ${registryUrl}`;
+ const composerRegistryIncludeStr = '{"type":"composer","url":"foo"}';
+ const composerPackageIncludeStr = JSON.stringify({
+ [packageWithoutBuildInfo.name]: packageWithoutBuildInfo.version,
+ });
describe('packagePipeline', () => {
it('should return the pipeline info when pipeline exists', () => {
@@ -214,4 +220,18 @@ describe('Getters PackageDetails Store', () => {
expect(pypiSetupCommand(state)).toBe(pypiSetupCommandStr);
});
});
+
+ describe('composer string getters', () => {
+ it('gets the correct composerRegistryInclude command', () => {
+ setupState({ composerPath: 'foo' });
+
+ expect(composerRegistryInclude(state)).toBe(composerRegistryIncludeStr);
+ });
+
+ it('gets the correct composerPackageInclude command', () => {
+ setupState();
+
+ expect(composerPackageInclude(state)).toBe(composerPackageIncludeStr);
+ });
+ });
});
diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js
index 61a5bb16edb..c0ae972d519 100644
--- a/spec/frontend/packages/shared/components/package_list_row_spec.js
+++ b/spec/frontend/packages/shared/components/package_list_row_spec.js
@@ -67,10 +67,6 @@ describe('packages_list_row', () => {
it('has project field', () => {
expect(findProjectLink().exists()).toBe(true);
});
-
- it('does not show the delete button', () => {
- expect(findDeleteButton().exists()).toBe(false);
- });
});
describe('showPackageType', () => {
@@ -96,9 +92,7 @@ describe('packages_list_row', () => {
});
describe('delete event', () => {
- beforeEach(() =>
- mountComponent({ isGroup: false, packageEntity: packageWithoutTags, shallow: false }),
- );
+ beforeEach(() => mountComponent({ packageEntity: packageWithoutTags, shallow: false }));
it('emits the packageToDelete event when the delete button is clicked', () => {
findDeleteButton().trigger('click');
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js
index b4c6d202e14..757a02a04a3 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/project_find_file_spec.js
@@ -1,11 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
-jest.mock('sanitize-html', () => jest.fn(val => val));
+jest.mock('dompurify', () => ({
+ sanitize: jest.fn(val => val),
+}));
const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`;
const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`;
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
index 71efcf8768e..e14c189d4b6 100644
--- a/spec/graphql/types/alert_management/alert_type_spec.rb
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -30,6 +30,8 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
metrics_dashboard_url
runbook
todos
+ details_url
+ prometheus_alert
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/issuable_connection_type_spec.rb b/spec/graphql/types/countable_connection_type_spec.rb
index af34611ecfe..af34611ecfe 100644
--- a/spec/graphql/types/issuable_connection_type_spec.rb
+++ b/spec/graphql/types/countable_connection_type_spec.rb
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index f7522cb3e2c..abeeeba543f 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -7,11 +7,76 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do
expected_fields = %w[
- name id state metrics_dashboard
+ name id state metrics_dashboard latest_opened_most_severe_alert
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_environment) }
+
+ context 'when there is an environment' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ name
+ state
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns an environment' do
+ expect(subject['data']['project']['environment']['name']).to eq(environment.name)
+ end
+
+ context 'when query alert data for the environment' do
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ environment(name: "#{environment.name}") {
+ name
+ state
+ latestOpenedMostSevereAlert {
+ severity
+ title
+ detailsUrl
+ prometheusAlert {
+ humanizedText
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'does not return alert information' do
+ expect(subject['data']['project']['environment']['latestOpenedMostSevereAlert']).to be_nil
+ end
+
+ context 'when alert is raised on the environment' do
+ let!(:prometheus_alert) { create(:prometheus_alert, project: project, environment: environment) }
+ let!(:alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert) }
+
+ it 'returns alert information' do
+ expect(subject['data']['project']['environment']['latestOpenedMostSevereAlert']['severity']).to eq(alert.severity.upcase)
+ end
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index b3dccde8ce3..b11951190e0 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -24,9 +24,11 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
source_branch_exists target_branch_exists
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees participants subscribed labels discussion_locked time_estimate
- total_time_spent reference author merged_at
+ total_time_spent reference author merged_at commit_count
]
+ expected_fields << 'approved_by' if Gitlab.ee?
+
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 5be1fafffb6..a0b6858fc99 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues
issue milestones pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
- boards jira_import_status jira_imports services releases release
+ environment boards jira_import_status jira_imports services releases release
alert_management_alerts alert_management_alert alert_management_alert_status_counts
container_expiration_policy sast_ci_configuration service_desk_enabled service_desk_address
issue_status_counts
@@ -98,6 +98,13 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) }
end
+ describe 'environment field' do
+ subject { described_class.fields['environment'] }
+
+ it { is_expected.to have_graphql_type(Types::EnvironmentType) }
+ it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver.single) }
+ end
+
describe 'members field' do
subject { described_class.fields['projectMembers'] }
diff --git a/spec/graphql/types/prometheus_alert_type_spec.rb b/spec/graphql/types/prometheus_alert_type_spec.rb
new file mode 100644
index 00000000000..716537ea716
--- /dev/null
+++ b/spec/graphql/types/prometheus_alert_type_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PrometheusAlert'] do
+ specify { expect(described_class.graphql_name).to eq('PrometheusAlert') }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ id humanized_text
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_prometheus_alerts) }
+end
diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
index 08a3fbd7867..45e87466532 100644
--- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb
@@ -19,6 +19,41 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
end
end
+ context 'when there is a <sources>' do
+ shared_examples_for 'ignoring sources' do
+ it 'parses XML without errors' do
+ expect { subject }.not_to raise_error
+
+ expect(coverage_report.files).to eq({})
+ end
+ end
+
+ context 'and has a single source' do
+ let(:cobertura) do
+ <<-EOF.strip_heredoc
+ <sources>
+ <source>project/src</source>
+ </sources>
+ EOF
+ end
+
+ it_behaves_like 'ignoring sources'
+ end
+
+ context 'and has multiple sources' do
+ let(:cobertura) do
+ <<-EOF.strip_heredoc
+ <sources>
+ <source>project/src/foo</source>
+ <source>project/src/bar</source>
+ </sources>
+ EOF
+ end
+
+ it_behaves_like 'ignoring sources'
+ end
+ end
+
context 'when there is a single <class>' do
context 'with no lines' do
let(:cobertura) do
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 7c22af54c71..f937a879400 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -230,6 +230,17 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to match_array(env_alert) }
end
+ describe '.order_severity_with_open_prometheus_alert' do
+ subject { described_class.where(project: alert_project).order_severity_with_open_prometheus_alert }
+
+ let_it_be(:alert_project) { create(:project) }
+ let_it_be(:resolved_critical_alert) { create(:alert_management_alert, :resolved, :critical, project: alert_project) }
+ let_it_be(:triggered_critical_alert) { create(:alert_management_alert, :triggered, :critical, project: alert_project) }
+ let_it_be(:triggered_high_alert) { create(:alert_management_alert, :triggered, :high, project: alert_project) }
+
+ it { is_expected.to eq([triggered_critical_alert, triggered_high_alert]) }
+ end
+
describe '.counts_by_status' do
subject { described_class.counts_by_status }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 9445ddfcd9d..91a669aa3f4 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -483,11 +483,7 @@ RSpec.describe Ci::JobArtifact do
subject { create(:ci_job_artifact, :archive) }
context 'when existing object has local store' do
- it 'is stored locally' do
- expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
- expect(subject.file).to be_file_storage
- expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
+ it_behaves_like 'mounted file in local store'
end
context 'when direct upload is enabled' do
@@ -496,11 +492,7 @@ RSpec.describe Ci::JobArtifact do
end
context 'when file is stored' do
- it 'is stored remotely' do
- expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(subject.file).not_to be_file_storage
- expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
- end
+ it_behaves_like 'mounted file in object store'
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c449a3c3c47..2696d144db4 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it { is_expected.to have_many(:deployments) }
it { is_expected.to have_many(:metrics_dashboard_annotations) }
it { is_expected.to have_many(:alert_management_alerts) }
+ it { is_expected.to have_one(:latest_opened_most_severe_alert) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
@@ -1347,4 +1348,27 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0 })
end
end
+
+ describe '#has_opened_alert?' do
+ subject { environment.has_opened_alert? }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment, reload: true) { create(:environment, project: project) }
+
+ context 'when environment has an triggered alert' do
+ let!(:alert) { create(:alert_management_alert, :triggered, project: project, environment: environment) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when environment has an resolved alert' do
+ let!(:alert) { create(:alert_management_alert, :resolved, project: project, environment: environment) }
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when environment does not have an alert' do
+ it { is_expected.to be(false) }
+ end
+ end
end
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 36d45f17392..a0f633218b0 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -152,14 +152,10 @@ RSpec.describe LfsObject do
end
describe 'file is being stored' do
- let(:lfs_object) { create(:lfs_object, :with_file) }
+ subject { create(:lfs_object, :with_file) }
context 'when existing object has local store' do
- it 'is stored locally' do
- expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL)
- expect(lfs_object.file).to be_file_storage
- expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
+ it_behaves_like 'mounted file in local store'
end
context 'when direct upload is enabled' do
@@ -167,13 +163,7 @@ RSpec.describe LfsObject do
stub_lfs_object_storage(direct_upload: true)
end
- context 'when file is stored' do
- it 'is stored remotely' do
- expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(lfs_object.file).not_to be_file_storage
- expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
+ it_behaves_like 'mounted file in object store'
end
end
end
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index 00e67ad70db..68bb86bfa49 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -45,9 +45,7 @@ RSpec.describe Terraform::State do
describe '#update_file_store' do
context 'when file is stored in object storage' do
- it 'sets file_store to remote' do
- expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
- end
+ it_behaves_like 'mounted file in object store'
end
context 'when file is stored locally' do
@@ -55,9 +53,7 @@ RSpec.describe Terraform::State do
stub_terraform_state_object_storage(Terraform::StateUploader, enabled: false)
end
- it 'sets file_store to local' do
- expect(subject.file_store).to eq(ObjectStorage::Store::LOCAL)
- end
+ it_behaves_like 'mounted file in local store'
end
end
end
diff --git a/spec/presenters/alert_management/alert_presenter_spec.rb b/spec/presenters/alert_management/alert_presenter_spec.rb
index 4281babee61..394007a802f 100644
--- a/spec/presenters/alert_management/alert_presenter_spec.rb
+++ b/spec/presenters/alert_management/alert_presenter_spec.rb
@@ -58,4 +58,10 @@ RSpec.describe AlertManagement::AlertPresenter do
expect(presenter.runbook).to eq('https://runbook.com')
end
end
+
+ describe '#details_url' do
+ it 'returns the details URL' do
+ expect(presenter.details_url).to match(%r{#{project.web_url}/-/alert_management/#{alert.iid}/details})
+ end
+ end
end
diff --git a/spec/presenters/prometheus_alert_presenter_spec.rb b/spec/presenters/prometheus_alert_presenter_spec.rb
new file mode 100644
index 00000000000..b9f18e2be28
--- /dev/null
+++ b/spec/presenters/prometheus_alert_presenter_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PrometheusAlertPresenter do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+
+ let(:presenter) { described_class.new(prometheus_alert) }
+
+ describe '#humanized_text' do
+ subject { presenter.humanized_text }
+
+ let_it_be(:prometheus_metric) { create(:prometheus_metric, project: project) }
+ let(:prometheus_alert) { create(:prometheus_alert, operator: operator, project: project, environment: environment, prometheus_metric: prometheus_metric) }
+ let(:operator) { :gt }
+
+ it { is_expected.to eq('exceeded 1.0m/s') }
+
+ context 'when operator is eq' do
+ let(:operator) { :eq }
+
+ it { is_expected.to eq('is equal to 1.0m/s') }
+ end
+
+ context 'when operator is lt' do
+ let(:operator) { :lt }
+
+ it { is_expected.to eq('is less than 1.0m/s') }
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
index da1fab42b1f..d3a2e6a1deb 100644
--- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -7,9 +7,9 @@ RSpec.describe 'getting Alert Management Alerts' do
let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' }, 'runbook' => 'runbook' } }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
- let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
- let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low).present }
+ let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload).present }
+ let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields).present }
let(:params) { {} }
@@ -75,6 +75,8 @@ RSpec.describe 'getting Alert Management Alerts' do
'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'metricsDashboardUrl' => nil,
+ 'detailsUrl' => triggered_alert.details_url,
+ 'prometheusAlert' => nil,
'runbook' => 'runbook'
)
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index e2255fdb048..bb63a5994b0 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -171,4 +171,43 @@ RSpec.describe 'getting merge request listings nested in a project' do
it_behaves_like 'searching with parameters'
end
+
+ describe 'fields' do
+ let(:requested_fields) { nil }
+ let(:extra_iid_for_second_query) { merge_request_c.iid.to_s }
+ let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } }
+
+ def execute_query
+ query = query_merge_requests(requested_fields)
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'when requesting `commit_count`' do
+ let(:requested_fields) { [:commit_count] }
+
+ it 'exposes `commit_count`' do
+ merge_request_a.metrics.update!(commits_count: 5)
+
+ execute_query
+
+ expect(results).to include(a_hash_including('commitCount' => 5))
+ end
+
+ include_examples 'N+1 query check'
+ end
+
+ context 'when requesting `merged_at`' do
+ let(:requested_fields) { [:merged_at] }
+
+ before do
+ # make the MRs "merged"
+ [merge_request_a, merge_request_b, merge_request_c].each do |mr|
+ mr.update_column(:state_id, MergeRequest.available_states[:merged])
+ mr.metrics.update_column(:merged_at, Time.now)
+ end
+ end
+
+ include_examples 'N+1 query check'
+ end
+ end
end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index c90f771335e..c969638614e 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -82,6 +82,26 @@ RSpec.describe EnvironmentEntity do
end
end
+ context 'with alert' do
+ let!(:environment) { create(:environment, project: project) }
+ let!(:prometheus_alert) { create(:prometheus_alert, project: project, environment: environment) }
+ let!(:alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert) }
+
+ it 'exposes active alert flag' do
+ project.add_maintainer(user)
+
+ expect(subject[:has_opened_alert]).to eq(true)
+ end
+
+ context 'when user does not have permission to read alert' do
+ it 'does not expose active alert flag' do
+ project.add_reporter(user)
+
+ expect(subject[:has_opened_alert]).to be_nil
+ end
+ end
+ end
+
context 'pod_logs' do
context 'with reporter access' do
before do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index b187025eb11..13da76263b1 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -3,22 +3,23 @@
require 'spec_helper'
RSpec.describe TodoService do
- let(:author) { create(:user) }
- let(:assignee) { create(:user) }
- let(:non_member) { create(:user) }
- let(:member) { create(:user) }
- let(:guest) { create(:user) }
- let(:admin) { create(:admin) }
- let(:john_doe) { create(:user) }
- let(:skipped) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:assignee) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:member) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:john_doe) { create(:user) }
+ let_it_be(:skipped) { create(:user) }
+
let(:skip_users) { [skipped] }
- let(:project) { create(:project, :repository) }
let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
- before do
+ before_all do
project.add_guest(guest)
project.add_developer(author)
project.add_developer(assignee)
@@ -456,7 +457,16 @@ RSpec.describe TodoService do
end
context 'leaving a note on a commit in a public project with private code' do
- let(:project) { create(:project, :repository, :public, :repository_private) }
+ let_it_be(:project) { create(:project, :repository, :public, :repository_private) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(author)
+ project.add_developer(assignee)
+ project.add_developer(member)
+ project.add_developer(john_doe)
+ project.add_developer(skipped)
+ end
it 'creates a todo for each valid mentioned user' do
expected_todo = base_commit_todo_attrs.merge(
@@ -492,7 +502,16 @@ RSpec.describe TodoService do
end
context 'leaving a note on a commit in a private project' do
- let(:project) { create(:project, :repository, :private) }
+ let_it_be(:project) { create(:project, :repository, :private) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(author)
+ project.add_developer(assignee)
+ project.add_developer(member)
+ project.add_developer(john_doe)
+ project.add_developer(skipped)
+ end
it 'creates a todo for each valid mentioned user' do
expected_todo = base_commit_todo_attrs.merge(
@@ -822,7 +841,17 @@ RSpec.describe TodoService do
end
describe '#new_note' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(author)
+ project.add_developer(assignee)
+ project.add_developer(member)
+ project.add_developer(john_doe)
+ project.add_developer(skipped)
+ end
+
let(:mention) { john_doe.to_reference }
let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") }
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index d101b092e7d..f4343b8b783 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -37,7 +37,7 @@ module CycleAnalyticsHelpers
end
def create_cycle(user, project, issue, mr, milestone, pipeline)
- issue.update(milestone: milestone)
+ issue.update!(milestone: milestone)
pipeline.run
ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb
index 1daa92e8ad4..db217250b17 100644
--- a/spec/support/helpers/design_management_test_helpers.rb
+++ b/spec/support/helpers/design_management_test_helpers.rb
@@ -35,9 +35,9 @@ module DesignManagementTestHelpers
def act_on_designs(designs, &block)
issue = designs.first.issue
- version = build(:design_version, :empty, issue: issue).tap { |v| v.save(validate: false) }
+ version = build(:design_version, :empty, issue: issue).tap { |v| v.save!(validate: false) }
designs.each do |d|
- yield.create(design: d, version: version)
+ yield.create!(design: d, version: version)
end
version
end
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 9072c41fe66..4895bc3ba15 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -10,7 +10,7 @@ module JiraServiceHelper
password = 'my-secret-password'
jira_issue_transition_id = '1'
- jira_tracker.update(
+ jira_tracker.update!(
url: url, username: username, password: password,
jira_issue_transition_id: jira_issue_transition_id, active: true
)
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 92f6d673255..1118cfcf7ac 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -40,7 +40,7 @@ module LoginHelpers
if user_or_role.is_a?(User)
user_or_role
else
- create(user_or_role)
+ create(user_or_role) # rubocop:disable Rails/SaveBang
end
gitlab_sign_in_with(user, **kwargs)
diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb
index 887d68de4e1..aee57b452fe 100644
--- a/spec/support/helpers/notification_helpers.rb
+++ b/spec/support/helpers/notification_helpers.rb
@@ -12,7 +12,7 @@ module NotificationHelpers
def create_global_setting_for(user, level)
setting = user.global_notification_setting
setting.level = level
- setting.save
+ setting.save!
user
end
@@ -27,7 +27,7 @@ module NotificationHelpers
def create_notification_setting(user, resource, level)
setting = user.notification_settings_for(resource)
setting.level = level
- setting.save
+ setting.save!
end
# Create custom notifications
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index bc31ee955c8..8a52a614821 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -37,7 +37,7 @@ module StubObjectStorage
Fog.mock!
::Fog::Storage.new(connection_params).tap do |connection|
- connection.directories.create(key: remote_directory)
+ connection.directories.create(key: remote_directory) # rubocop:disable Rails/SaveBang
# Cleanup remaining files
connection.directories.each do |directory|
diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb
new file mode 100644
index 00000000000..397e22ace28
--- /dev/null
+++ b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+shared_examples 'N+1 query check' do
+ it 'prevents N+1 queries' do
+ execute_query # "warm up" to prevent undeterministic counts
+
+ control_count = ActiveRecord::QueryRecorder.new { execute_query }.count
+
+ search_params[:iids] << extra_iid_for_second_query
+ expect { execute_query }.not_to exceed_query_limit(control_count)
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb
new file mode 100644
index 00000000000..4cb087c47ad
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'mounted file in local store' do
+ it 'is stored locally' do
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+end
+
+RSpec.shared_examples 'mounted file in object store' do
+ it 'is stored remotely' do
+ expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(subject.file).not_to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 732a84299c5..c05f247332a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4150,6 +4150,11 @@ domhandler@^3.0.0:
dependencies:
domelementtype "^2.0.1"
+dompurify@^2.0.11:
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.11.tgz#cd47935774230c5e478b183a572e726300b3891d"
+ integrity sha512-qVoGPjIW9IqxRij7klDQQ2j6nSe4UNWANBhZNLnsS7ScTtLb+3YdxkRY8brNTpkUiTtcXsCJO+jS0UCDfenLuA==
+
domutils@^1.5.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"