From c787c1559e9e558b83e78354823eb54b9fe8c718 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 13 Aug 2020 18:10:36 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo.yml | 7 - .../components/ci_variable_modal.vue | 15 +- .../javascripts/frequent_items/utils.js | 4 +- .../incidents/components/incidents_list.vue | 48 +++- app/assets/javascripts/incidents/constants.js | 8 +- .../queries/get_count_by_status.query.graphql | 9 + .../queries/get_incidents.query.graphql | 4 +- .../issue_show/utils/parse_data.js | 2 +- .../javascripts/lib/utils/datetime_utility.js | 13 + app/assets/javascripts/lib/utils/highlight.js | 4 +- .../javascripts/notebook/cells/markdown.vue | 81 +++--- .../notebook/cells/output/html.vue | 7 +- .../packages/details/store/getters.js | 2 +- .../shared/components/package_list_row.vue | 7 +- app/assets/javascripts/project_find_file.js | 2 +- app/assets/javascripts/user_popovers.js | 2 +- app/assets/stylesheets/framework/filters.scss | 17 ++ .../stylesheets/framework/variables.scss | 2 + .../concerns/resolves_merge_requests.rb | 3 + .../types/alert_management/alert_type.rb | 10 + app/graphql/types/ci/pipeline_type.rb | 2 + ...n_type.rb => countable_connection_type.rb} | 3 +- app/graphql/types/environment_type.rb | 5 + app/graphql/types/issue_type.rb | 2 +- app/graphql/types/merge_request_type.rb | 13 +- app/graphql/types/project_type.rb | 6 + app/graphql/types/prometheus_alert_type.rb | 20 ++ app/models/alert_management/alert.rb | 3 +- app/models/ci/job_artifact.rb | 11 +- app/models/concerns/file_store_mounter.rb | 21 ++ app/models/deployment.rb | 2 +- app/models/environment.rb | 5 + app/models/lfs_object.rb | 11 +- app/models/merge_request.rb | 4 + app/models/prometheus_alert.rb | 1 + app/models/terraform/state.rb | 11 +- app/policies/prometheus_alert_policy.rb | 5 + .../alert_management/alert_presenter.rb | 10 +- app/presenters/prometheus_alert_presenter.rb | 18 ++ app/serializers/environment_entity.rb | 6 + ...your-group-level-package-registry-view.yml | 5 + changelogs/unreleased/232580-state-count.yml | 5 + ...ose-more-data-for-mr-metrics-dashboard.yml | 5 + .../unreleased/eb-skip-cobertura-sources.yml | 5 + ...graphql-api-for-alerts-in-environments.yml | 5 + .../group-coverage-reporting-csv.yml | 5 + changelogs/unreleased/rails-save-bang-12.yml | 5 + ...sabrams-fix_composer_installation_code.yml | 5 + .../sh-refactor-file-store-mounter.yml | 5 + ...ecify-ruby-image-in-fail-fast-template.yml | 5 + config/dependency_decisions.yml | 7 + config/webpack.vendor.config.js | 2 +- .../graphql/reference/gitlab_schema.graphql | 85 +++++++ doc/api/graphql/reference/gitlab_schema.json | 231 ++++++++++++++++++ doc/api/graphql/reference/index.md | 14 ++ doc/api/personal_access_tokens.md | 28 ++- doc/api/vulnerabilities.md | 2 +- doc/api/vulnerability_findings.md | 2 +- doc/ci/parent_child_pipelines.md | 4 +- doc/development/documentation/styleguide.md | 8 +- .../incident_management/img/incident_list.png | Bin 0 -> 34194 bytes doc/operations/incident_management/index.md | 24 +- doc/operations/metrics/alerts.md | 13 +- .../img/panel_context_menu_v13_0.png | Bin 34737 -> 0 bytes .../img/panel_context_menu_v13_3.png | Bin 0 -> 14538 bytes doc/operations/metrics/dashboards/index.md | 7 +- .../metrics/img/linked_runbooks_on_charts.png | Bin 0 -> 16966 bytes ...v13_1.png => vulnerability_page_v13_1.png} | Bin .../security_dashboard/index.md | 2 +- ...lity_page_download_patch_button_v13_1.png} | Bin ...e_merge_request_button_dropdown_v13_1.png} | Bin ...ility_page_merge_request_button_v13_1.png} | Bin ...v13_1.png => vulnerability_page_v13_1.png} | Bin .../vulnerabilities/index.md | 12 +- .../group/saml_sso/group_managed_accounts.md | 7 +- doc/user/project/integrations/webhooks.md | 52 ++++ doc/user/project/issues/design_management.md | 5 +- .../merge_requests/fail_fast_testing.md | 13 +- .../load_performance_testing.md | 3 +- .../test_coverage_visualization.md | 4 + lib/api/admin/ci/variables.rb | 18 +- lib/api/ci/pipeline_schedules.rb | 34 +-- lib/api/ci/pipelines.rb | 28 +-- lib/api/ci/runners.rb | 4 +- lib/api/entities/bridge.rb | 9 - lib/api/entities/ci/bridge.rb | 11 + lib/api/entities/ci/job.rb | 15 ++ lib/api/entities/ci/job_artifact.rb | 11 + lib/api/entities/ci/job_artifact_file.rb | 12 + lib/api/entities/ci/job_basic.rb | 20 ++ lib/api/entities/ci/job_basic_with_project.rb | 11 + lib/api/entities/ci/pipeline.rb | 19 ++ lib/api/entities/ci/pipeline_basic.rb | 16 ++ lib/api/entities/ci/pipeline_schedule.rb | 14 ++ .../entities/ci/pipeline_schedule_details.rb | 12 + lib/api/entities/ci/variable.rb | 14 ++ lib/api/entities/commit_detail.rb | 2 +- lib/api/entities/deployment.rb | 2 +- lib/api/entities/job.rb | 13 - lib/api/entities/job_artifact.rb | 9 - lib/api/entities/job_artifact_file.rb | 10 - lib/api/entities/job_basic.rb | 18 -- lib/api/entities/job_basic_with_project.rb | 9 - lib/api/entities/job_request/dependency.rb | 2 +- lib/api/entities/merge_request.rb | 4 +- lib/api/entities/package/pipeline.rb | 2 +- lib/api/entities/pipeline.rb | 17 -- lib/api/entities/pipeline_basic.rb | 14 -- lib/api/entities/pipeline_schedule.rb | 12 - lib/api/entities/pipeline_schedule_details.rb | 10 - lib/api/entities/variable.rb | 12 - lib/api/group_variables.rb | 18 +- lib/api/job_artifacts.rb | 4 +- lib/api/jobs.rb | 32 +-- lib/api/merge_requests.rb | 8 +- lib/api/triggers.rb | 4 +- lib/api/variables.rb | 18 +- lib/gitlab/ci/parsers/coverage/cobertura.rb | 2 + .../Load-Performance-Testing.gitlab-ci.yml | 2 +- .../templates/Verify/FailFast.gitlab-ci.yml | 1 + .../Load-Performance-Testing.gitlab-ci.yml | 2 +- .../metrics/subscribers/active_record.rb | 2 +- locale/gitlab.pot | 36 +++ package.json | 2 +- spec/factories/merge_requests.rb | 15 ++ spec/fixtures/api/schemas/environment.json | 1 + .../components/ci_variable_modal_spec.js | 26 +- .../components/incidents_list_spec.js | 44 +++- .../lib/utils/datetime_utility_spec.js | 14 ++ .../cells/output/html_sanitize_fixtures.js | 114 +++++++++ .../cells/output/html_sanitize_tests.js | 68 ------ .../notebook/cells/output/html_spec.js | 17 +- .../notebook/cells/output/index_spec.js | 2 +- .../packages/details/store/getters_spec.js | 20 ++ .../components/package_list_row_spec.js | 8 +- spec/frontend/project_find_file_spec.js | 6 +- .../types/alert_management/alert_type_spec.rb | 2 + ...c.rb => countable_connection_type_spec.rb} | 0 spec/graphql/types/environment_type_spec.rb | 67 ++++- spec/graphql/types/merge_request_type_spec.rb | 4 +- spec/graphql/types/project_type_spec.rb | 9 +- .../types/prometheus_alert_type_spec.rb | 17 ++ .../ci/parsers/coverage/cobertura_spec.rb | 35 +++ spec/models/alert_management/alert_spec.rb | 11 + spec/models/ci/job_artifact_spec.rb | 12 +- spec/models/environment_spec.rb | 24 ++ spec/models/lfs_object_spec.rb | 16 +- spec/models/terraform/state_spec.rb | 8 +- .../alert_management/alert_presenter_spec.rb | 6 + .../prometheus_alert_presenter_spec.rb | 32 +++ .../project/alert_management/alerts_spec.rb | 8 +- .../graphql/project/merge_requests_spec.rb | 39 +++ spec/serializers/environment_entity_spec.rb | 20 ++ spec/services/todo_service_spec.rb | 55 ++++- .../helpers/cycle_analytics_helpers.rb | 2 +- .../helpers/design_management_test_helpers.rb | 4 +- spec/support/helpers/jira_service_helper.rb | 2 +- spec/support/helpers/login_helpers.rb | 2 +- spec/support/helpers/notification_helpers.rb | 4 +- spec/support/helpers/stub_object_storage.rb | 2 +- ...merge_request_n_plus_one_query_examples.rb | 11 + .../file_store_mounter_shared_examples.rb | 17 ++ yarn.lock | 5 + 163 files changed, 1685 insertions(+), 560 deletions(-) create mode 100644 app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql rename app/graphql/types/{issuable_connection_type.rb => countable_connection_type.rb} (88%) create mode 100644 app/graphql/types/prometheus_alert_type.rb create mode 100644 app/models/concerns/file_store_mounter.rb create mode 100644 app/policies/prometheus_alert_policy.rb create mode 100644 app/presenters/prometheus_alert_presenter.rb create mode 100644 changelogs/unreleased/227714-delete-packages-from-your-group-level-package-registry-view.yml create mode 100644 changelogs/unreleased/232580-state-count.yml create mode 100644 changelogs/unreleased/233942-expose-more-data-for-mr-metrics-dashboard.yml create mode 100644 changelogs/unreleased/eb-skip-cobertura-sources.yml create mode 100644 changelogs/unreleased/extend-graphql-api-for-alerts-in-environments.yml create mode 100644 changelogs/unreleased/group-coverage-reporting-csv.yml create mode 100644 changelogs/unreleased/rails-save-bang-12.yml create mode 100644 changelogs/unreleased/sabrams-fix_composer_installation_code.yml create mode 100644 changelogs/unreleased/sh-refactor-file-store-mounter.yml create mode 100644 changelogs/unreleased/specify-ruby-image-in-fail-fast-template.yml create mode 100644 doc/operations/incident_management/img/incident_list.png delete mode 100644 doc/operations/metrics/dashboards/img/panel_context_menu_v13_0.png create mode 100644 doc/operations/metrics/dashboards/img/panel_context_menu_v13_3.png create mode 100644 doc/operations/metrics/img/linked_runbooks_on_charts.png rename doc/user/application_security/security_dashboard/img/{standalone_vulnerability_page_v13_1.png => vulnerability_page_v13_1.png} (100%) rename doc/user/application_security/vulnerabilities/img/{standalone_vulnerability_page_download_patch_button_v13_1.png => vulnerability_page_download_patch_button_v13_1.png} (100%) rename doc/user/application_security/vulnerabilities/img/{standalone_vulnerability_page_merge_request_button_dropdown_v13_1.png => vulnerability_page_merge_request_button_dropdown_v13_1.png} (100%) rename doc/user/application_security/vulnerabilities/img/{standalone_vulnerability_page_merge_request_button_v13_1.png => vulnerability_page_merge_request_button_v13_1.png} (100%) rename doc/user/application_security/vulnerabilities/img/{standalone_vulnerability_page_v13_1.png => vulnerability_page_v13_1.png} (100%) delete mode 100644 lib/api/entities/bridge.rb create mode 100644 lib/api/entities/ci/bridge.rb create mode 100644 lib/api/entities/ci/job.rb create mode 100644 lib/api/entities/ci/job_artifact.rb create mode 100644 lib/api/entities/ci/job_artifact_file.rb create mode 100644 lib/api/entities/ci/job_basic.rb create mode 100644 lib/api/entities/ci/job_basic_with_project.rb create mode 100644 lib/api/entities/ci/pipeline.rb create mode 100644 lib/api/entities/ci/pipeline_basic.rb create mode 100644 lib/api/entities/ci/pipeline_schedule.rb create mode 100644 lib/api/entities/ci/pipeline_schedule_details.rb create mode 100644 lib/api/entities/ci/variable.rb delete mode 100644 lib/api/entities/job.rb delete mode 100644 lib/api/entities/job_artifact.rb delete mode 100644 lib/api/entities/job_artifact_file.rb delete mode 100644 lib/api/entities/job_basic.rb delete mode 100644 lib/api/entities/job_basic_with_project.rb delete mode 100644 lib/api/entities/pipeline.rb delete mode 100644 lib/api/entities/pipeline_basic.rb delete mode 100644 lib/api/entities/pipeline_schedule.rb delete mode 100644 lib/api/entities/pipeline_schedule_details.rb delete mode 100644 lib/api/entities/variable.rb create mode 100644 spec/frontend/notebook/cells/output/html_sanitize_fixtures.js delete mode 100644 spec/frontend/notebook/cells/output/html_sanitize_tests.js rename spec/graphql/types/{issuable_connection_type_spec.rb => countable_connection_type_spec.rb} (100%) create mode 100644 spec/graphql/types/prometheus_alert_type_spec.rb create mode 100644 spec/presenters/prometheus_alert_presenter_spec.rb create mode 100644 spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb create mode 100644 spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 21784a993b1b7..5a1fc68a915e5 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 14c71f7329173..fbf19847e9d83 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 { diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js index d4b7ffdcbe112..112e8eaaf1741 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 85f24c5b122ea..ecd8acb449e76 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 {
- - + + diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js index fe92f13173898..dc90f30991c2f 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 0000000000000..0b784b104a84f --- /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 6e8e6a1254c76..0f56e8640bd1c 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 05e384adad3f4..8cd1c1b0e56d2 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 08daac1575468..e26b63fbb8545 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 b1dd562f63a42..32553af9af3af 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 `` elements. @@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '', 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 fcb09ea90db18..fa1afdcd16faf 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,6 +1,6 @@ @@ -111,7 +108,7 @@ export default {
@@ -126,7 +123,7 @@ export default {
-
+
(iid) { where(iid: iid) } scope :for_status, -> (status) { where(status: status) } @@ -137,6 +137,7 @@ class Alert < ApplicationRecord # 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 de7bd9fb67b7e..75c3ce98c959d 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -9,6 +9,7 @@ class JobArtifact < ApplicationRecord include Sortable include IgnorableColumns include Artifactable + include FileStoreMounter extend Gitlab::Ci::Model NotSupportedAdapterError = Class.new(StandardError) @@ -115,7 +116,7 @@ class JobArtifact < ApplicationRecord 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 @@ class JobArtifact < ApplicationRecord 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 @@ def validate_file_format! 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 0000000000000..9d4463e52976c --- /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 87587bb5afac7..d6508ffcebab3 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -148,7 +148,7 @@ def short_sha 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 bddc84f10b531..c6a08c996dae4 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 @@ def has_sample_metrics? !!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 3761484b15d7b..d60baa299cbb3 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 1c95789d7baeb..f4c2d568b4daa 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -264,10 +264,14 @@ def public_merge_status 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 1c870f4391a6d..80eef1705e715 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 759b9ce1eec08..c50b9da131099 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 @@ class State < ApplicationRecord 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 0000000000000..e6b0e6e8c171f --- /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 c3067e6377fc2..5bfa6dee18b81 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 @@ def runbook 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 0000000000000..99e24bdcdb9c2 --- /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 7da5910a75bfc..a2bf9716f8f53 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 @@ def can_read_pod_logs? 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 0000000000000..e40960f1e6c3a --- /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 0000000000000..9fc05eecdab37 --- /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 0000000000000..85f0dd4080f88 --- /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 0000000000000..fe0ed7d364883 --- /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 0000000000000..5f60e9b6c51b3 --- /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 0000000000000..9f9b0546e2475 --- /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 0000000000000..5e4f8ccf90058 --- /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 0000000000000..367d8a73baa89 --- /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 0000000000000..19d8b346ab4af --- /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 0000000000000..1a565bba4ea44 --- /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 790b09c1dfaf5..d6386329d8347 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 548eca4200f6f..29c4c33314e74 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 82344869fa101..92766ab68e4b6 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -209,6 +209,11 @@ type AlertManagementAlert implements Noteable { """ details: JSON + """ + The URL of the alert detail page + """ + detailsUrl: String! + """ All discussions on this noteable """ @@ -294,6 +299,11 @@ type AlertManagementAlert implements Noteable { last: Int ): NoteConnection! + """ + The alert condition for Prometheus + """ + prometheusAlert: PrometheusAlert + """ Runbook for the alert as defined in alert details """ @@ -4418,6 +4428,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 """ @@ -8240,6 +8255,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 """ @@ -8270,6 +8310,11 @@ type MergeRequest implements Noteable { """ author: User + """ + Number of commits in the merge request + """ + commitCount: Int + """ Timestamp of when the merge request was created """ @@ -10249,6 +10294,11 @@ type Pipeline { The connection type for Pipeline. """ type PipelineConnection { + """ + Total count of collection + """ + count: Int! + """ A list of edges. """ @@ -10553,6 +10603,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 """ @@ -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 0f13079f20209..719d448b5f4f3 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -577,6 +577,24 @@ "isDeprecated": false, "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", @@ -801,6 +819,20 @@ "isDeprecated": false, "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", @@ -12338,6 +12370,20 @@ "isDeprecated": false, "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", @@ -22902,6 +22948,59 @@ "isDeprecated": false, "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", @@ -22969,6 +23068,20 @@ "isDeprecated": false, "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", @@ -30645,6 +30758,24 @@ "name": "PipelineConnection", "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.", @@ -31457,6 +31588,57 @@ "isDeprecated": false, "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", @@ -35655,6 +35837,55 @@ "enumValues": null, "possibleTypes": null }, + { + "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", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 932135d443d6e..cadcacb7f488b 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 162ba88f727b8..517e26f3d85aa 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: " "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: " "https://gitlab.example.com/api/v4/personal_access_tokens/" +``` + +### 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 70f29d961e369..a0d871af127a1 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 e21d903e4747f..96171f0229d54 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 a8ae49543a626..1cfa698bfa54f 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. 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 1c77878882781..c252f6425d016 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 0000000000000000000000000000000000000000..0498fec6c9cc714a8b7e5ac84e72b26a3f91bac3 GIT binary patch literal 34194 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 literal 0 HcmV?d00001 diff --git a/doc/operations/incident_management/index.md b/doc/operations/incident_management/index.md index 5db6c76a42b1a..a44d2cc080777 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 6b5cbab83990b..2ed8de9396abb 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 2d7cb9239811ca8709b5a39189c8f3a7d3565f3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34737 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

Q$gM=g3-<&ioYgg(QOR9sN!l`PF zCApg?ZAEoFp>MZt45!32?nq+t5a*hOt(W_dyUk+?_v$N_f4@vT>P;|<%sY|pn3ygq zH(C=4m7_82(62f=37@o51IK<~r(b5;7M{E{|LF7ture)}Bp<$dALTgsx5w}512>Ef zG+T4XDZ(4;@=j@nMs!MjB$G!ycH2r3%tf|m+^q+^I5gytLcWJKH}8%@(Dq)=0ZC)? z`9~QB0oi%JFZ&Pu3o>7MEIyy4w+|ONDoN+@v#S)KNZ|bcc=Zygzkf}0@eTM6MWS1} zTN(!i9Ss$UncNYQ9lQ zO?&iJr~Z>-TZOEM54XyrsL58jy#Jd*;$6vi^bjfh!%*Ksq8nXkqu)FPND+6gl!|v( zt2Nkc?Z>j|Z%G{Zh#r1au_PG?aZvZEi|E@8RM>NU1LJg-`VatrHchN4`GQYFN4J! z&i+VCVNeGLuz6k#Jo}g~mA)q$z;oCG9cByO?^86v#YGk;rc=|zm%x1W4i6I27-bEZQJ#W?kCYvp^?XeN* z%RO2$L1oRvx8&rfF{)t5k=tWXo|nHzP}*J|(-TErMrTr)F#rPI2p73uxOJWWv^tI}C;3g(UQHKzuGxkFx^?Z}U3feGqhh#iG zh;nN3O++|Tx*?6Cw^Pg5dhDxZn3?<+g4;pSpjpP03m9WKA#3ziWb%uj z0c}jkg_)QZ;IF;DAT`kVs6qY%P7B323Cwg`5-X*w?DMpi;*twR;OnRUpEh@2y~pvd zE}!i?Q+~VruqK!J^(U#;?mMqhjZD85?VHzVtdBt+dToORz-k*#jk>VCh1aG-o4p1H zJf=b!`vu|N!Bf55a=fnP3U*vYS=VpNTuWR6LoB1j&tH*CZv>!}QLW4s@t2SFN9Bqy z@gEmGMylo*mB}i`f+A(MKN9qz#44=;@{Ug>-Q56P@C;u5LUly7b8Xn;sK1$;_f39;D}jpi2|2u3j{BdgGBQg@hi0q=GgV z9<;E{+Pw|YxhS(tO+LuJNM|y&gByo47oo$3H1(Ti8ljxq*VZZgTHZiOu))n?e|-A$ zC(5SdPz_;zhYdAb+yTzfrw{%tuUU?x_GM!N?;ZQy`gw8ZBl^-d!}!;j&({lV^;AYzoLKUgB?7afB(sOf%(~^P3;4+?kg@q>76$i zym=hm@$o%8O_(p!S}I~+*p!$|{W7FXnR5Z4K4^nWtFA`&m~^zpgF6lOI_h@8B6hM7 z>}O`r*s(cwAZxvlciyYUf)^%1d3B^-!;~k?kEw6IDw(u+8EEHB)BG)LB zjoYz7nG4T z`H!1t-yC0Vx=8#&g30jNv|p=TlZpmOnO6Y;C;rZi>nQ`#c1R%2>h|ePBkpj|HQ$K^wh=NwGNK{0%?rnj^GJB+;b1&N zK=Vv_W*mjiirpfv%b?79#6I-O!ky!-R#r@b#EgwF#X!SGU>t14`RkNJps zH+M-_()`Kn;1Y)>g(_}~r&ePJ;>`#5Eva@__xcLC{(h>G^XBPjK$8D(iOKdw-AtL+ zU9tE#9>FM!2clE@Yn=M-`@YXf)n}*Uyw_^xl*$eO&D-v|xn3@wt~=2EhL$nKNNjq$ zj)62iT1cLEaI(_&O7Wc|nbP@ANz)Cf@X6PxT2eHM3}mkKryt*xQj?+*5gu6E*&Py*=!}nY;1KpDf0HBrjw@lZWe$+4Q3I}pZFY&7 z3dvqPta%0BqLI?zm3$%JN&yKgl@b)KQnoM?&04b)2)=Qzmty68vOi$F)(u$xdCDYQ zOu%{g0ph#OV?w>f_50-dmH0~Pm_L1~5u#&}EB>3uHR5?iRa%4!LLSd&Ty$9XD~{%y zEKLoa68WE8-scTqt-Zii??3wZnh8g>T=O)Nyt6zNTSZ1jatEJt(=9$}a79yl zg=wP;{;HmjIj9?!F8-S3@PR3!HqByXk@c&=VVj$+eE-4)z59#}Na$c7!t}$yR!zrd zVA`GA-^Y8$kwu zDc2$==TX_02JzU{dOg(Y5_}{)D|Sf*(hBLF*2o)3*mEf|3rYPVXCpGtt49AEy`Hq5 z57(TOVIOS6B=w~at7_j`|2}nvG`~5B*H8h-cuT6(WCIk0UzD2j+4?CxX26vS3!FRx zIq`ggI7Pspf2ZCyYKZ3R+zYI!OW87@6DLiq7}E^@Vlds{Tl_W|OZyAfb$3zUrztHrHKOfN zY(uIRyo;|tLh~Asj@IU#Oohf|+9Fr3rLj_OJpI`|4qqX*E0sZ!2ZQfmMr(T=)(oQa z-x$=w;zp~%iQ#&~AdzpIy-?%f`HJhY7TNL|qibkEYHr=~V=yP8~0C^~KSxlC4#^={uA^H*1J?%-YwaN5V0k4wT`{Ydgrbk_lS$w`h%bz-I>)`JW-j(5^D*IDW0vLVIl3z zjU%t`%SXQS!L~*OrNv&ri3R+C?A)@; z-=3o77q{SSj=gl!T&SDYwo=rmt>tZVVm zH%A-IBtvtZ(SWNpM4*6np!HN{$djtsO86vCZ)*ulU)tmw`f*Y4hQk|>et?-0{?KH= zb}$}oy?Mo`rOvFNJYbOOC`EZw7|=LY!Ca?L$+<9x-RPk_DkqWabNH{ifA;M z*JtqK7B3m;|3GcZOri;-jW@z-m$;LHf9BqoCw?rQ*ki}Lj+SdECHtZG7c~FbybCp{Jb(RaT;xf{qxVffAWVw_pFWP7=Du{@8XlU> z`T2#cn%Lj>+oI_3nB9caq+^7>!#Cho+D@wN9zR}5Cfk0! z_JRZ9r&0Y#-0Ya1#U$k>p~O*|H^(XUwX8_w_S`UEYx?b5oH`DoaNin1t-MYpo6@#>1aW6=i7{ZGvHct%RyM+G2lVi(z*7 z-7-7X7raU1@W52VtFbn)!Cm{?oGcOD;$f`ZQW#y9x%UB`R|5R4O#?98);_%Ce38~r>gk>!rse=Uv?l~VtRpZarm z9WlA9x%D>uj(|vIxS%5umE73HKn>Q2otu{z0KUdSwh$X(pJ9~VA zR4RL#e`z6JG3)?)N6!UHhlf<|gYW_TnQ_{t$G15u6(_@^5thG_A*&;OBw>8eIw|X7 zjraxl)FHA%M;}s~L%@uH5VnUEDlUkC19**xfY6O%iu6BkAHeV7ag6`DA-u*5Lq|Y> zSNt#8|0?@Gefax-$gDNpUBrB&S`7?zJ;RR0Kv>gm7Q(zcb-(y?Lregt1dc`WSCjK? zV`9F}_28^Gm8jn((LZq4cUkWsk;i+RH>*LoGAGNe-u%&@@0Oj9l&D@`HLY+3-9M0T zq9XX@cW-GlpZpmUyE~s0=@G$L1s|(ocP}|3Ca@bk8?GQkaLpv0VxRdjggD}ksh7?xd$KXGcOcdjEKYo=<0qHz?^Xx88G1^> z|0yNbcG*i;@uFhn*Ek#rhsZ@#u=J5YMl1N{GRJ>Jb{p`YK4DfMqJ33n;9GSus- zVKqiKI}Vo|S?^rg$E^6L)b4ZSl~3CVKBEKQ;B9cCR7<*x{5?2`xrbE*^~{GR1Gdg% zhP=~SyGgFtrrLuW?+7|LU*gEK&l0|vg0K5h(lxM8=pbd;z~Ink%NORxWK+&ACXB;( zovzVAXm5HRgUyn1g9YCH+V|Y6vK5pBeNkA-Ft3r{;m(+UaXY~s{Aznt;=-OhsF}^s z4+2~~$oWqD5C!3r^yj<9OsNv^FhB2_l?ZUhfAjrS0g%3rju4Fpd%@2KkM2-jAMdRV zka|m~@3;^bP~Dgl=&N(nxpmQo_nrHN2KezVv2J@X%S$^eZP8Ttqu*9?cq_Z$-^KTA zYlw5N{}45rq=`qrfsKUlg=1jU^wnG2_mxSkcW)E~94*p3dCnG|{V1)UztF4$15HRf zw}sDA$#~C3qT=O2E!y=F=B%QlLjwV&aV2~7C!h|F_YZWv(WDd)7#2}dPv?$VW+4gp zgBHSO`)7{hDVdi9@lv%Ui=Ad>;FB2JvtB4xUwc`s$ob&RW_a^!&OV?oH#yOjaPq;q z0#pH%CD6~}PNha3_1BR!-vKz^Z#2Sgzcof~YTK?!`E8%Yw>c|dvwLOD3jKS&GG}?Q z>!gDmh$9;-X{ha|IbF}6D)0(YOqA&_4u}!#iQf6`ZSNG@OuO+*LQA(<_upNzw~T=Y z?!ye#pI9mIlQ|15>BFG;Wv?whr6PVx2pGLRUipnh@ot0qlT9AYhxx!lltT7GQhvem z{m8AGUJF6=IWls4PLi{I3hc_W*N}qG+?wu~QBm=+3=pS!dc#KsPZGjp7C`=*k~pct zx{+~Ke#o~T@gB48ahoY!lCQsE&c2f8xnm!vfu3_*>B-fDlH?{-n{O5&K2qcahZJ{1 zT3Cw7$ovVMR53>;<1$J}|*gYu)}Odg#ao z@Wn^F3aQV;g;82nRB;gbsa8>W<}dEoc<3+qkkE}9h( z@{H-b;Ea21n5wTPfi0^O{#!W--BiW_`L_tG+T`5|W2d{?bRk`G#WmJny1@Z_kD+Fc zY#aNo#UU}S5(_jFt?c372{W3)szmRE^`fCRC8d!Hm@9H|OSzJH9sM zOr0JfS#J4)>lO_Poh;+|ZCc?Q-)!yWKsv2CxbwTAy!TS2Fqa@BZh;ZvgefFWT20o> z#&uw~wm;(;EKCQut0Ak68n)teGz0ELqhrjLHrZRbf{q^$gK|j-@hsg|=vh3_4$&4- zThMMQC?yNP6=!>Cv=H0O>pm9M@Ov=iH;8ELs@ZBwp#@OOr6rRvf%i_%`wep2fQ&IS z=kmc{Fg7c=hVe-%=m>+ts@;pvSjl{_Kh2#9fZyM=bj4g4Z@iRxND1!y0>0PW?bi8= zPX{q7A95^)+PIh>5QFvuJ{`%w^4<>hR_0)ZnC|1d^jQ>6A@<@f^PfmE^*jc}pmUTD zdMNCQddT{kE%@))(X&Fh#0py+i{94L&_Hrc6;|1%X9t$}qZEI5Y2MQJWS9nrJB)0L znd4|qU7VC?viHX7A(dV`$tPWP1HW}=3@jVUacZTM15#z|j}Da>nLH9%;TPR*4gez~R^qFR(Y>QdTS)Z^e zr>_FZ6^0tREo0svDE$GEvas zFmE2P2jB5bpguLFeBdW~Lo%V+A1Pz&?16n1Y^Q@LeRRk`sO(;89d0ttGy_ZeT$utU zJ!kt9@BQ4bsbGr&uy$uOW*+!2(ni$K`W9`uEHwfG%CUm9grwGrD%e}5>D-O9(R2b!$e=5iv&TR(WyaF1VPLyym1=aw(Cm%>K`h^mIL-}sPXcc(j8<;v?q}*1w{Bf|@vFUs_(*UXzB*R7;N)b5 z*l?=xCq=!J#lV#K6Z6WCBQNT@mk!V=l8+fk z2N5FID16A!vmyB|?H)egUm8>1JLO&0c@4jjqgN~7yE7y{;0!aBGI%wyQDXJLI6KMcogzhY*?kah&A?0h2vO53^c; zUJz6u850Eic#u^p8*Rs}{TJD0QcG5CzomzGpmiiZS6oplW1r@gP4 zIF*i~*7iN(qTtl1nf;U={SlAn<+01p`=omxk=o<3%MbDEfUY}e*ng0wN~uh8QmYwdw1C)WZF^0mEdJ`SClnL1%BktYZIhY?N+t-&Vtg%_cqiVBRLpZ( z&F3g4s6{7d&?kbgMbb>~X1V2|_x#tp|FLxvnrfnMI&lF3d&y}V zbbg8FZSeW|XmacnIW!NPHjnaN=5Tu=3lF-+3z`CihQZg-pi1} z^s%#C`nb&9%%qhKXTsLro-OiG_VUkq#tTc2lq^=-7xm>}r?z43m17H|gM=zqeGnH< zeWCLBa2?OoZ91zQS&xQ%H+5NAuDNsKgM!I3`a+VYS_DMt70U}B{&7S0{vNHgz3)Rz zl5QU4bM||$nZGPrzm0WialY!!Pp97g;?JAt&OYt-eTQU=&Xrfs^gmlY@9vzt{C>Oc ze}47t*S+@z6Wa96G#8vnoBXE}*l?Y`{NMV-UBbmi#|+AES5NPI`BJbZCiiOQSp%NO z$@^W!%f0jNuD8#0PP2|r4Cm_RGFsiwJdvUu41CyhO zgW%0apQ{;hQRG~qdqD2eX)xF`*gNm&x?pO zf6n~v%4=zjjmx%7UEaUyOLpeDgGVI^^rz6J8|-&79(%BS{-b55K7r&sUHx3v0nZx( E0K~H|eE^T{Je#$0*h z@rG{oQRyQJN<%#Moe9R{7}ZTpMjWMflIqtZy=bGM36N zg@PjNCGMtxuuYrr1ZbdA4eke*6!}kLR?&)o}QeZe4I|MR$M&q-@oVL=H=q$<#^QK zaPxL_H}T?dbYu9JlmGG~Y2jw(YUAu~7 z+rQI#OpxoZgo}rho9n-QKbi{vHekp z`FSJ~d3$?%d3l+Ss+5G{ zXBQV2x3;zdfj~9YQ61Ipqr;QX(9rt&`tI)Tsqc%ry1GNn<6krS4E4Sz1a!K&xyh)^ zXliPTii%1p&3f8`)6&w`q1&nc4IUmIO}Sm*uLF)oEvc!gqu-`~^v<@G59fbr^0sW8 z8-^68)P}fqnCo^%d(>v5Dt!9%$#L_}X?#phpnqIVK_l|uxVgGbiznnB1%(zxPV%FM*D^d8rcR+t(u+4HB2h4XFeWuF z?gP}G=WkM=jlq5Wkr(3&kvM+wx=dwcZE3h7w}nFybKKT=oLp&{tw2jE>?_QBW!P^y zz**%?sVGqq$5gN<0@RSODu%MMfl3GaIqC{;-Q<-T-#UNb&PflSSim0p zx2<~^>qe66L4Rv~6o#QVaX)|EVx-sBwMtiV_`6*VEh5A06agogxf_|h2L`sA&@`%h zZW72-MHP(E+{FM9N$RjPk2+Kcr*LHN^ES&o&3v;wd)z1%3F? zSM!0^BL&1!UIMfbDNLU=PEa$-+A!cF!(niGZkEk&K%O3sH$V9cOK6_F_6-F-wq=Vd zu~7v{^iOol-%W5f#l8ue!E_o_FGr(-_;Q3{U|0HB!II;bX+e~ns-xK_fK!ZERU%Hp1@jNmU5tsE#jf(*XIl|F^=&1Z{itD{Z7=az_BIrRcz4M-MoIdd zC+OYJxQjL{YxF2}4!8;>I*(j9^2^b==!+76=g(7b)PJeYxE~xHTAILYnHw1@!;v9i zh$M-F+?6_6nNgG^)yz5)tuw#L0%@FQ5^&i%80mru21Em$4Z*a%gp)QilUUQSb+&VH z2hZtRTSH%KzufqOufRB+rKo17h$MBb6GLi;m`bw=ZrfUZEORn~rIFKJ{-IYoIV{9c zR!yLCjjC7rO0zM>LB?wDt`?4^&dJ}m%&*6!UrHTIUlKb#k1FyUbJ^WU4$Q7+z3FcV zaCOCAkp(+_R9E#*2L+A!1cz&=qLTOsW5kiqu+6ygIu(AhJTr}6uPh21l0sqI zErRWpT5Hcv%3vD}y|y8JCQDVB+fkHPU?QLpqNZ8c-ThVNDDqE&gdvHXH_KbYgO%7S zhNm677Zu@t+S@@se)_ksxPBaJsD`#lC`ci4B#3XRl3k&PpxtV0xvP-xq)AnWhw&XH z*4s%%@_?#fUz8{FOVKuQ7CcLB>PjjIC+i3~?qycmvJ+qGal1p%#`z4(C(|1@Yc<=h zZ>E}wit5^QWNBQI2K*IS;?N>c{0>PNfc0u16(0k1)M}8FR83VFb1+5R5fUnaqT&2l zGM1VY;;VD_(dbWv+URkWW2c!3MP-sm-emV(`=I?;Y`S(n8dX8*<^hJyvmxvoo8k_y zfDh3PJ`!Qh<(^u)C1op|!z`N!50m1! z7Qk=gtJ3ZcknRIh#G|MdHgCV2xvUFVjBBwQs|B9f4CewCkwnwAz6@gB(+hxs_<9q> zK@o6MH5(vyu@|&ZD{%jk#7-LzA>godx(u{d2tvN&NsDT!Qb=mqX*qO5L0|dC{?blc z1@YyWKQ-4BaZr`(ScyoC`xhBd@M-uUlZb4^dt^B`7}QzwTV=VKh176cXK1EYba?`8 z)q3uy?Ogk$S_8;qCe8$*^LJ`y%0>X0*0RCiDP6!WY#Hk2w6I)dMHspBtfTp4i`#R3cW~*?`84D`rh8b-IAE*6v(V#s2iX7iypSpP^7* z4@!mLx{u+oOY5)ODF*um24|O6z!EO$+ZZLsQTVW0oKPWJ>t&Y$;@T;XsV>$!L=;pX zdPko#;N4@Kym6f&06k0T2`%64&bychavb@F|@kQlBg>k7T1oS{)Viq_>#s} zQ>&Z(RQ8oC4a16Vzw91UUL7@d_cx+LNLRuha-ZW_uj^9H({ZU}fs(`QjAfCm-u--q7AUQ;?lb@w> z{*3#Dsgt2zP!-yDPN8T|V>eoX2iib?)Lp7_;NsRn8{Ri)tXj zEK<^S2C>?^ocjbp$Zs$dAY}HK;M_cNE~Lfm-}^}i>{bChp%ccj(|(5-!`m>?0e9MH z`(WbrFE8+oF8{OXe6wFYxZ*q*BXq#=mN2}U;S(Y|&&d|MRhhNB@{51~k*vj@vtm!d zWj{v$mD`A^xZMb!a6lZWyb1>ISU~32JXjzmD?u-&RB9eXrNLq?_m3MHA&T4M^oPN2 zc)0;oP5ocDt5LhFy2T#;P|#@Zme%r@dQg52D8HpOuMl+d(Byd~=0Igb)%)sbXex*! z&Czd&i%jB+kpQ!H2+t?Dpm9QjqStem?|?8eoY3^AIkW_7XeFbik_M%34Za!%L@8^Q z%9r0qziWad;M?oT3xFquu^OAq*p0=;;p~8)Qr86y338`{@k4NdQes>1Ok;^G zjT`i)F8Yr2z}#wp|0bjnoLrBxcPycVm1JYk`lZbk{*FwdveG1O*Pg-~zAyQC)mPxOkD(BJJDcAT4YJfIlZGfU5p+#hPN znJG|gg%!R_)MFtDt$uulx7M6+J_6)du{N2X*@N2pCeYX{H`l)53iKQ?DqYyBRbl)H z2;TSFepX1>duv6sbIfSYMhk&CCNh)nPf0dfwV;0ndMx4dg;i+F<=~i}ID5^v>9`9vT-Corg00(AIgRuoA|{#YWTnQ%q2E19&Q2EcIVh&CUnJf)%JT;UM{fD{x# zdnw2Eh;cb1n1;B4i_wQ1>}GKl-oU04VhuY|*j&m-2j|~dVKsmUATO&azIFAG^*KfD z`UdnrWToe-*$`V99zPJl3KI%>IP1zrs!ac4SX!)YNfM+)!0e4|4 z6!0h(r?w&h*iL;LNG}~&rhIB{4WYf4edlEQNnbrGY-(^iAd3uq#(r-OqVfA|No92R z5){w9XnT+-gc?re(!Ze1YMs8HwkT|7Om>Y#={a}vxUC&GjL zin~l9V%a$i7aYZ#4v%jhaKygq;2z@cgRl|F#9)2Ze?(YU8QWgn8){ zUC6tUqh-w@Wjc`NevL#Y<{Odc5y2fQn}#<*huZyg0wZq*90j*&{j^RCwhrf9_TtnNs_28V6# zm=qMjjE=LbTTeh+yK0$83BN2y*-h@uKc=NZxJmcifOnb7cb2~-#mvf4z$ba6AxW9W zS-f?f|zv`!Z%`fJ0Wl>K2iMcFkMvsbx@3~D&wM@}kEv@Jb z3BVbzRfld`Z4$!Tt1Jps#Ite~pzo$YssF;cZ03Qw>K6@cUD#y7%To@FeH)dz`pfj4 z2vBhgnP!;C;(MB8K`CGp13XE^f{5(hES2B6RDWAQsSTq$R;WF)ab3v+T&_eK*gH3?Zy`Npqa8QRf?W@tioGpAsM+~gGrww%3P6h z$P54j%Iao@HH1vji;v78gMIEXIXOaIfgS3_tg((LXA7&R5{+7420`k1a6RH@9AB|l~@u==x{#dg>KHW*by8)mjjj_7*obmaj#r@T{89@G@ zAU*FeKv_)Hc zw+cZ{Vt{>mgWSln0oS3p2K5AB1U0pVU|$@948R2Sv*8GfyH%rpJZM>m;?5SM4C~%S zI{ZCK$iP11X(w~WS8NaA4ZpA9G@!DE3C@GfYH=%?*OPL6QvoaHi-CEQfZ1nO4_}N{L#u)N-b}cu6J6G`lahE&?-SKS^g1{0F zj=?fyTofP-l$i1D9HP{EuxH8ueNJ+%vf3 zKR!a^VSL|_H@<)U$HX1C_|HiAFrJ?aY<*w@o*pUg_<*$xf?1n6l#A0F!SN!^eZSFI zv}oUeE^iy;{VNS~4DE-V`obr=MCU`)aV*8v&w&hJ?QQL<^jwR#JYFK_9NeH`$RFoL zuJzxqyzYN~zW67S54ZdiH^o5nUj9%vx)~H9mqTjMbKF|9q zo5YjWH_oWr_jP`FdgAp#*+=!1bW;*4y_W%($f3$eMr;(XWIC=7ll5kfiu9}>U+@G5 zejH#RFeyzy!@z1Fku*q1Xi~<0DexsYu)6WfSVRN8WyC#@hyv&)cR2`c(E|dVzn-^*WZr z!mRY6JCj?9NTwc4QsK6<%d#!NV(dmA-Uxfm*cZ^p02_`|GwqO9`j4BU(epC|wh|%R zMB|^c{>q6)ZGM-Qh%vyiD%{oKZC!&hgXXX~!@c>y9%Tw& zTpQZn0e9R2u{fT(j-su$-F>UWzo`d@yzv6%3RjW~9(g~{7IJ!anY7o0Xxa){w0{TA zqnaN;TiUl6cFL`Xe_spRGsU$`53TWRji8Q4wj4dzG9eijE_ZpgNyK8HYJQqOmyV>D zYyR5LdhdDtxzVSGnO>-;99ctjkw8BV^s3GJ$^(ehJ7VV=XL<^LVX1@*DzQ}iv>9NS zpP-Jqx<&YF$7c0;m^uL*H()|$eQ~+?#w;UOvc}VO%d6@93mp9?l&DFcM4RuqaIOX> z_<8AXaEofZS~Rfy_uHx7MSxcSf~8WEMvdogQetlJS;9Lz=7#ze6S`nn#i+Rt+GXz? zeRWtEb8Mw-(IViL99xzkMt|thx3fVN@t-3y(mKg`4}PW@M*c6Ln-Q&Rkt8042Ov+E zq|}Ow$-WXw#3FhusGFZiXgRXeSha4+lWfGHM!X@p;zIefVTCIYc8=E1eI%Y|c@dsj z3`ZYFd~cadD|<8*gyGTW!ONc5G6#iGjB%RV5T}Kv4{{QOV?Mxf^@{u<`8N- zFHUX25(gxYVHXHsaN>*FHg7XBmYv2QjGovz6-ZRF1$~~7q!JqAw9N^Vsvr4G zn)ho-eRNFOYeEtoJok%rD$X4vaPYJD VVMzc1!$?%e*WHI&+<>#8)`k$=o$6Dvv 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 L%96F>#=-v!^%91{ literal 0 HcmV?d00001 diff --git a/doc/operations/metrics/dashboards/index.md b/doc/operations/metrics/dashboards/index.md index 11d2dc45008ec..ffcb7dc92c681 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 0000000000000000000000000000000000000000..335ba5dc172a4dc53fbf0636dedd676f04eab309 GIT binary patch literal 16966 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 [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 08455dc472533..126970ebbb6a7 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 d8e8ab2fab7df..800eb1d335976 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 4e7d460e7c719..371469a6ed614 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 619a6d0457779..60f81159394e8 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 3239269109d4f..97f4f202ab376 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 793cedb0210db..6751dde155c95 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 6b0ff5e9395cb..8721d94d642cf 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -12,7 +12,7 @@ class Variables < Grape::API::Instance 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 @@ class Variables < Grape::API::Instance 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 @@ class Variables < Grape::API::Instance 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 @@ class Variables < Grape::API::Instance 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 @@ class Variables < Grape::API::Instance 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 80ad8aa04ddd3..1afdb0ad34cfd 100644 --- a/lib/api/ci/pipeline_schedules.rb +++ b/lib/api/ci/pipeline_schedules.rb @@ -12,7 +12,7 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance .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 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance 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 @@ class PipelineSchedules < Grape::API::Instance 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 4fb301f026003..bbbf3b683c528 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -13,7 +13,7 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance .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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 @@ class Pipelines < Grape::API::Instance 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 2c156a7116069..7bca72f802813 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -111,7 +111,7 @@ class Runners < Grape::API::Instance 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 @@ class Runners < Grape::API::Instance 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 8f0ee69399ad1..0000000000000 --- 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 0000000000000..502d97fff902b --- /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 0000000000000..7fe1a802e2479 --- /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 0000000000000..9e504aee3830d --- /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 0000000000000..418eb408ab6fc --- /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 0000000000000..a29788c7abff9 --- /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 0000000000000..736e611e5b17e --- /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 0000000000000..3dd3b9c9eff9e --- /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 0000000000000..dbb9b828757d0 --- /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 0000000000000..f1596b7d285a0 --- /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 0000000000000..b233728b95b8b --- /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 0000000000000..f4d5248245a4e --- /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 22424b38bb978..61238102e9d8a 100644 --- a/lib/api/entities/commit_detail.rb +++ b/lib/api/entities/commit_detail.rb @@ -9,7 +9,7 @@ class CommitDetail < Commit 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 3a97d3e3c091c..4e3a4c289d982 100644 --- a/lib/api/entities/deployment.rb +++ b/lib/api/entities/deployment.rb @@ -6,7 +6,7 @@ class Deployment < Grape::Entity 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 cbee879400723..0000000000000 --- 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 94dbdb38fee68..0000000000000 --- 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 fa2851a7f0e20..0000000000000 --- 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 a8541039934f1..0000000000000 --- 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 09387e045ec31..0000000000000 --- 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 64d779f657588..7d6ec832ba15e 100644 --- a/lib/api/entities/job_request/dependency.rb +++ b/lib/api/entities/job_request/dependency.rb @@ -5,7 +5,7 @@ module Entities 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 7fc76a4071ecb..05ae041c7a9e8 100644 --- a/lib/api/entities/merge_request.rb +++ b/lib/api/entities/merge_request.rb @@ -23,11 +23,11 @@ class MergeRequest < MergeRequestBasic 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 e91a12e47faf1..0aa888e30eeeb 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 778efbe4bcc54..0000000000000 --- 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 359f6a447abba..0000000000000 --- 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 a72fe3f314176..0000000000000 --- 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 5e54489a0f90f..0000000000000 --- 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 6705df30b2e54..0000000000000 --- 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 b5ff151f07dd5..e7b8cd10197b6 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -13,18 +13,18 @@ class GroupVariables < Grape::API::Instance 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 @@ class GroupVariables < Grape::API::Instance 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 @@ class GroupVariables < Grape::API::Instance ).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 @@ class GroupVariables < Grape::API::Instance ).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 @@ class GroupVariables < Grape::API::Instance # 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 61c279a76e9fa..bc7bc956580e0 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -94,7 +94,7 @@ def authorize_download_artifacts! 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 @@ def authorize_download_artifacts! 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 9fab722b72e34..084c146abe7d1 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -30,7 +30,7 @@ class Jobs < Grape::API::Instance end desc 'Get a projects jobs' do - success Entities::Job + success Entities::Ci::Job end params do use :optional_scope @@ -44,12 +44,12 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 @@ class Jobs < Grape::API::Instance 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 53a7a2498a64b..6f25df720c47f 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -352,16 +352,16 @@ def authorize_push_to_merge_request!(merge_request) 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 @@ def authorize_push_to_merge_request!(merge_request) 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 de67a14927497..f398bbf3e32f1 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -11,7 +11,7 @@ class Triggers < Grape::API::Instance 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 @@ class Triggers < Grape::API::Instance 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 50d137ec7c1dd..6f449fd060aca 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -30,18 +30,18 @@ def find_variable(params) 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 @@ def find_variable(params) 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 @@ def find_variable(params) 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 @@ def find_variable(params) 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 @@ def find_variable(params) # 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 006d5097148b6..934c797580c35 100644 --- a/lib/gitlab/ci/parsers/coverage/cobertura.rb +++ b/lib/gitlab/ci/parsers/coverage/cobertura.rb @@ -28,6 +28,8 @@ def parse_all(root, coverage_report) 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 b437ddbd734e6..4a9849c85c9f4 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 77a1b57d92ff4..584e6966180fd 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 d39bd23402086..f964b3b2caf52 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 346a2f9a4613e..e53ac00e77f02 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -22,7 +22,7 @@ def sql(event) 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 e6dfda7dec624..38120b51b0f2d 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 cb19b90d0faf7..5f70378f735d7 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 6fe5c9e0ff90d..af6e88f73b1e3 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -43,6 +43,21 @@ 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 f42d701834aaf..d1274bea8179d 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 ad398d6ccd66c..4e35243f48493 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 ec42df0b0c375..aabafaa91543b 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 87e6d2724f617..d9866a94ffe68 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 0000000000000..a886715ce4bf6 --- /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: `foo`, + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: simple, spaces before', + { + input: `foo`, + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: simple, spaces after', + { + input: `foo`, + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: simple, spaces before and after', + { + input: `foo`, + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: preceding colon', + { + input: `foo`, + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: UTF-8 encoding', + { + input: 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: long UTF-8 encoding', + { + input: 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: long UTF-8 encoding without semicolons', + { + input: + 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: hex encoding', + { + input: 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: long hex encoding', + { + input: 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: hex encoding without semicolons', + { + input: + 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: null char', + { + input: 'foo', + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: invalid URL char', + { input: '', output: '' }, + ], + [ + 'protocol-based JS injection: Unicode', + { + input: `foo`, + output: 'foo', + }, + ], + [ + 'protocol-based JS injection: spaces and entities', + { + input: `foo`, + output: 'foo', + }, + ], + [ + 'img on error', + { + input: '', + output: '', + }, + ], + ['style tags are removed', { input: ' 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 74c48f0436751..0000000000000 --- 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: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: simple, spaces before': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: simple, spaces after': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: simple, spaces before and after': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: preceding colon': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: UTF-8 encoding': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: long UTF-8 encoding': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: long UTF-8 encoding without semicolons': { - input: - 'foo', - output: 'foo', - }, - 'protocol-based JS injection: hex encoding': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: long hex encoding': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: hex encoding without semicolons': { - input: - 'foo', - output: 'foo', - }, - 'protocol-based JS injection: null char': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: invalid URL char': { - input: '', - output: '', - }, - 'protocol-based JS injection: Unicode': { - input: 'foo', - output: 'foo', - }, - 'protocol-based JS injection: spaces and entities': { - input: 'foo', - output: 'foo', - }, - 'img on error': { - input: '', - output: '', - }, -}; diff --git a/spec/frontend/notebook/cells/output/html_spec.js b/spec/frontend/notebook/cells/output/html_spec.js index 3ee404fb187b8..48d62d74a5009 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 2b1aa5317c53c..b9a2dfb8f34d7 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 95eb5abd0cfec..307976d412486 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 -Password `; 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 61a5bb16edb7f..c0ae972d51946 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 b4c6d202e1472..757a02a04a340 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 71efcf8768ee7..e14c189d4b6ae 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 @@ 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 similarity index 100% rename from spec/graphql/types/issuable_connection_type_spec.rb rename to 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 f7522cb3e2c1b..abeeeba543f95 100644 --- a/spec/graphql/types/environment_type_spec.rb +++ b/spec/graphql/types/environment_type_spec.rb @@ -7,11 +7,76 @@ 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 b3dccde8ce3f0..b11951190e0de 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -24,9 +24,11 @@ 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 5be1fafffb6ed..a0b6858fc99c7 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -24,7 +24,7 @@ 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 @@ 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 0000000000000..716537ea71679 --- /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 08a3fbd7867e9..45e87466532b7 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 @@ end end + context 'when there is a ' 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 + + project/src + + EOF + end + + it_behaves_like 'ignoring sources' + end + + context 'and has multiple sources' do + let(:cobertura) do + <<-EOF.strip_heredoc + + project/src/foo + project/src/bar + + EOF + end + + it_behaves_like 'ignoring sources' + end + end + context 'when there is a single ' 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 7c22af54c71ba..f937a8794002d 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -230,6 +230,17 @@ 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 9445ddfcd9df0..91a669aa3f4ae 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -483,11 +483,7 @@ 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 @@ 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 c449a3c3c475c..2696d144db48e 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -19,6 +19,7 @@ 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 @@ 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 36d45f1739221..a0f633218b07f 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -152,14 +152,10 @@ 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 @@ 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 00e67ad70db01..68bb86bfa49c5 100644 --- a/spec/models/terraform/state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -45,9 +45,7 @@ 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 @@ 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 4281babee61d6..394007a802fcc 100644 --- a/spec/presenters/alert_management/alert_presenter_spec.rb +++ b/spec/presenters/alert_management/alert_presenter_spec.rb @@ -58,4 +58,10 @@ 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 0000000000000..b9f18e2be280f --- /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 da1fab42b1f5f..d3a2e6a1debe2 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 @@ 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 @@ '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 e2255fdb048d7..bb63a5994b0ec 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 @@ def query_merge_requests(fields) 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 c90f771335e50..c969638614ee5 100644 --- a/spec/serializers/environment_entity_spec.rb +++ b/spec/serializers/environment_entity_spec.rb @@ -82,6 +82,26 @@ 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 b187025eb1110..13da76263b1c5 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 @@ 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 @@ 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 @@ 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 d101b092e7dd5..f4343b8b783e0 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -37,7 +37,7 @@ def create_commit(message, project, user, branch_name, count: 1, commit_time: ni 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 1daa92e8ad442..db217250b17a0 100644 --- a/spec/support/helpers/design_management_test_helpers.rb +++ b/spec/support/helpers/design_management_test_helpers.rb @@ -35,9 +35,9 @@ def url_for_designs(issue) 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 9072c41fe6668..4895bc3ba1536 100644 --- a/spec/support/helpers/jira_service_helper.rb +++ b/spec/support/helpers/jira_service_helper.rb @@ -10,7 +10,7 @@ def jira_service_settings 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 92f6d673255b4..1118cfcf7acab 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -40,7 +40,7 @@ def gitlab_sign_in(user_or_role, **kwargs) 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 887d68de4e137..aee57b452fe4c 100644 --- a/spec/support/helpers/notification_helpers.rb +++ b/spec/support/helpers/notification_helpers.rb @@ -12,7 +12,7 @@ def send_notifications(*new_mentions) 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 @@ def create_user_with_notification(level, username, resource = project) 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 bc31ee955c8ee..8a52a61482117 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -37,7 +37,7 @@ def stub_object_storage(connection_params:, remote_directory:) 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 0000000000000..397e22ace284d --- /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 0000000000000..4cb087c47adf4 --- /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 732a84299c579..c05f247332aab 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"