From 68c476dbd8a2c670aeeebffce8b63b554a3ac7f0 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 17 May 2021 15:10:15 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/build-images.gitlab-ci.yml | 12 +- .gitlab/ci/global.gitlab-ci.yml | 2 +- .rubocop_manual_todo.yml | 1 - .rubocop_todo.yml | 1 - GITALY_SERVER_VERSION | 2 +- .../design_management/pages/index.vue | 2 +- .../components/time_tracking/report.vue | 12 +- .../components/time_tracking/time_tracker.vue | 2 +- .../dropdown_contents_labels_view.vue | 2 +- app/controllers/dashboard_controller.rb | 2 +- app/controllers/groups_controller.rb | 3 +- app/controllers/projects_controller.rb | 3 +- app/finders/packages/group_packages_finder.rb | 1 - app/finders/packages/nuget/package_finder.rb | 1 - app/finders/packages/package_finder.rb | 1 - app/finders/packages/packages_finder.rb | 1 - app/finders/packages/pypi/package_finder.rb | 2 +- app/finders/packages/pypi/packages_finder.rb | 2 +- .../chat_message/alert_message.rb | 76 +++++ .../integrations/chat_message/base_message.rb | 88 ++++++ .../chat_message/deployment_message.rb | 87 ++++++ .../chat_message/issue_message.rb | 74 +++++ .../chat_message/merge_message.rb | 83 +++++ .../integrations/chat_message/note_message.rb | 86 ++++++ .../chat_message/pipeline_message.rb | 267 ++++++++++++++++ .../integrations/chat_message/push_message.rb | 120 ++++++++ .../chat_message/wiki_page_message.rb | 63 ++++ app/models/packages/package.rb | 5 - app/models/pages/lookup_path.rb | 9 +- app/models/pages/virtual_domain.rb | 4 +- .../chat_message/alert_message.rb | 74 ----- .../chat_message/base_message.rb | 86 ------ .../chat_message/deployment_message.rb | 85 ------ .../chat_message/issue_message.rb | 72 ----- .../chat_message/merge_message.rb | 81 ----- .../chat_message/note_message.rb | 84 ----- .../chat_message/pipeline_message.rb | 265 ---------------- .../chat_message/push_message.rb | 118 ------- .../chat_message/wiki_page_message.rb | 61 ---- .../chat_notification_service.rb | 14 +- app/models/project_services/slack_service.rb | 2 +- app/services/issuable/destroy_service.rb | 20 +- app/services/spam/spam_verdict_service.rb | 16 +- ...merge_request_draft_status_email.html.haml | 4 +- .../shared/_issuable_meta_data.html.haml | 2 +- ...d-drop-message-in-markdown-and-designs.yml | 5 + ...acy_storage-feature-flag-on-gitlab-com.yml | 5 + ...le-serving-pages-from-legacy-storage-2.yml | 5 + .../325689-remove-delete-async-ffs.yml | 5 + .../329521-store-segment-target-groups.yml | 5 + .../330845-observe-limit-to-hours.yml | 5 + ...add_duplicate_notes_note_trigram_index.yml | 5 + .../cablett-change-draft-status-email.yml | 5 + .../grape-action-caching-default.yml | 5 + ...nicolasdular-email-campaign-usage-data.yml | 5 + .../api_caching_rate_limit_branches.yml | 2 +- .../destroy_issuable_label_links_async.yml | 8 - .../destroy_issuable_todos_async.yml | 8 - .../pages_serve_from_legacy_storage.yml | 8 - .../pages_update_legacy_storage.yml | 8 - ..._product_marketing_email_create_0_sent.yml | 21 ++ ...t_marketing_email_create_0_cta_clicked.yml | 22 ++ ..._product_marketing_email_create_1_sent.yml | 21 ++ ...t_marketing_email_create_1_cta_clicked.yml | 22 ++ ..._product_marketing_email_create_2_sent.yml | 21 ++ ...t_marketing_email_create_2_cta_clicked.yml | 21 ++ ..._product_marketing_email_verify_0_sent.yml | 21 ++ ...t_marketing_email_verify_0_cta_clicked.yml | 21 ++ ..._product_marketing_email_verify_1_sent.yml | 21 ++ ...t_marketing_email_verify_1_cta_clicked.yml | 22 ++ ..._product_marketing_email_verify_2_sent.yml | 21 ++ ...t_marketing_email_verify_2_cta_clicked.yml | 21 ++ ...n_product_marketing_email_trial_0_sent.yml | 21 ++ ...ct_marketing_email_trial_0_cta_clicked.yml | 21 ++ ...n_product_marketing_email_trial_1_sent.yml | 21 ++ ...ct_marketing_email_trial_1_cta_clicked.yml | 22 ++ ...n_product_marketing_email_trial_2_sent.yml | 21 ++ ...ct_marketing_email_trial_2_cta_clicked.yml | 21 ++ ...in_product_marketing_email_team_0_sent.yml | 21 ++ ...uct_marketing_email_team_0_cta_clicked.yml | 21 ++ ...in_product_marketing_email_team_1_sent.yml | 21 ++ ...uct_marketing_email_team_1_cta_clicked.yml | 22 ++ ...in_product_marketing_email_team_2_sent.yml | 21 ++ ...uct_marketing_email_team_2_cta_clicked.yml | 21 ++ ...0210430122951_add_snapshot_namespace_id.rb | 7 + ...12_add_display_namespace_id_to_segments.rb | 7 + ...10430124630_add_devops_adoption_indexes.rb | 32 ++ ...30130259_remove_obsolete_segments_field.rb | 18 ++ ...134202_copy_adoption_snapshot_namespace.rb | 16 + ...135954_copy_adoption_segments_namespace.rb | 14 + ...210511051718_create_index_on_notes_note.rb | 26 ++ db/schema_migrations/20210430122951 | 1 + db/schema_migrations/20210430124212 | 1 + db/schema_migrations/20210430124630 | 1 + db/schema_migrations/20210430130259 | 1 + db/schema_migrations/20210430134202 | 1 + db/schema_migrations/20210430135954 | 1 + db/schema_migrations/20210511051718 | 1 + db/structure.sql | 20 +- .../monitoring/prometheus/gitlab_metrics.md | 1 + doc/administration/pages/index.md | 52 +++- doc/api/epic_links.md | 2 +- .../testing_guide/best_practices.md | 21 ++ doc/development/usage_ping/dictionary.md | 288 ++++++++++++++++++ doc/user/group/epics/img/epic_view_v13.0.png | Bin 51112 -> 0 bytes .../epics/img/new_epic_from_groups_v13.7.png | Bin 10505 -> 0 bytes doc/user/group/epics/index.md | 157 ++-------- doc/user/group/epics/manage_epics.md | 73 ++++- doc/user/project/issues/managing_issues.md | 7 +- .../references/snippet_reference_filter.rb | 8 +- .../database/reindexing/concurrent_reindex.rb | 18 +- lib/gitlab/database/with_lock_retries.rb | 4 +- .../with_lock_retries_outside_transaction.rb | 41 +++ lib/gitlab/pages/settings.rb | 4 - lib/gitlab/pages/stores/local_store.rb | 15 - lib/gitlab/usage_data.rb | 25 +- locale/gitlab.pot | 5 +- qa/qa.rb | 1 + qa/qa/resource/group.rb | 43 ++- qa/qa/resource/group_base.rb | 74 +++++ qa/qa/resource/sandbox.rb | 38 +-- .../1_manage/group/bulk_import_group_spec.rb | 39 ++- scripts/trigger-build | 2 +- spec/features/groups/issues_spec.rb | 2 +- .../filtered_search/filter_issues_spec.rb | 2 +- spec/features/issues/service_desk_spec.rb | 2 + .../packages/group_packages_finder_spec.rb | 2 +- spec/finders/packages/package_finder_spec.rb | 2 +- spec/finders/packages/packages_finder_spec.rb | 2 +- .../components/time_tracking/report_spec.js | 30 +- .../filter/references/reference_cache_spec.rb | 1 + .../snippet_reference_filter_spec.rb | 27 ++ .../reindexing/concurrent_reindex_spec.rb | 36 ++- ...h_lock_retries_outside_transaction_spec.rb | 244 +++++++++++++++ .../gitlab/database/with_lock_retries_spec.rb | 18 +- spec/lib/gitlab/pages/settings_spec.rb | 8 - .../gitlab/pages/stores/local_store_spec.rb | 25 -- spec/lib/gitlab/usage_data_spec.rb | 80 +++++ ...2_copy_adoption_snapshot_namespace_spec.rb | 47 +++ ...4_copy_adoption_segments_namespace_spec.rb | 25 ++ .../chat_message/alert_message_spec.rb | 2 +- .../chat_message/base_message_spec.rb | 2 +- .../chat_message/deployment_message_spec.rb | 2 +- .../chat_message/issue_message_spec.rb | 2 +- .../chat_message/merge_message_spec.rb | 2 +- .../chat_message/note_message_spec.rb | 2 +- .../chat_message/pipeline_message_spec.rb | 2 +- .../chat_message/push_message_spec.rb | 2 +- .../chat_message/wiki_page_message_spec.rb | 2 +- spec/models/packages/package_spec.rb | 16 - spec/models/pages/lookup_path_spec.rb | 7 +- .../microsoft_teams_service_spec.rb | 2 +- .../spam/spam_verdict_service_spec.rb | 31 +- spec/spec_helper.rb | 5 +- .../destroy_service_shared_examples.rb | 40 --- ...quest_draft_status_email.html.haml_spec.rb | 3 + 156 files changed, 2904 insertions(+), 1427 deletions(-) create mode 100644 app/models/integrations/chat_message/alert_message.rb create mode 100644 app/models/integrations/chat_message/base_message.rb create mode 100644 app/models/integrations/chat_message/deployment_message.rb create mode 100644 app/models/integrations/chat_message/issue_message.rb create mode 100644 app/models/integrations/chat_message/merge_message.rb create mode 100644 app/models/integrations/chat_message/note_message.rb create mode 100644 app/models/integrations/chat_message/pipeline_message.rb create mode 100644 app/models/integrations/chat_message/push_message.rb create mode 100644 app/models/integrations/chat_message/wiki_page_message.rb delete mode 100644 app/models/project_services/chat_message/alert_message.rb delete mode 100644 app/models/project_services/chat_message/base_message.rb delete mode 100644 app/models/project_services/chat_message/deployment_message.rb delete mode 100644 app/models/project_services/chat_message/issue_message.rb delete mode 100644 app/models/project_services/chat_message/merge_message.rb delete mode 100644 app/models/project_services/chat_message/note_message.rb delete mode 100644 app/models/project_services/chat_message/pipeline_message.rb delete mode 100644 app/models/project_services/chat_message/push_message.rb delete mode 100644 app/models/project_services/chat_message/wiki_page_message.rb create mode 100644 changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml create mode 100644 changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml create mode 100644 changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml create mode 100644 changelogs/unreleased/325689-remove-delete-async-ffs.yml create mode 100644 changelogs/unreleased/329521-store-segment-target-groups.yml create mode 100644 changelogs/unreleased/330845-observe-limit-to-hours.yml create mode 100644 changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml create mode 100644 changelogs/unreleased/cablett-change-draft-status-email.yml create mode 100644 changelogs/unreleased/grape-action-caching-default.yml create mode 100644 changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml delete mode 100644 config/feature_flags/development/destroy_issuable_label_links_async.yml delete mode 100644 config/feature_flags/development/destroy_issuable_todos_async.yml delete mode 100644 config/feature_flags/development/pages_serve_from_legacy_storage.yml delete mode 100644 config/feature_flags/development/pages_update_legacy_storage.yml create mode 100644 config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml create mode 100644 config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml create mode 100644 config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml create mode 100644 config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml create mode 100644 config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml create mode 100644 config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml create mode 100644 config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml create mode 100644 config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml create mode 100644 config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml create mode 100644 config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml create mode 100644 config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml create mode 100644 config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml create mode 100644 config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml create mode 100644 config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml create mode 100644 db/migrate/20210430122951_add_snapshot_namespace_id.rb create mode 100644 db/migrate/20210430124212_add_display_namespace_id_to_segments.rb create mode 100644 db/migrate/20210430124630_add_devops_adoption_indexes.rb create mode 100644 db/post_migrate/20210430130259_remove_obsolete_segments_field.rb create mode 100644 db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb create mode 100644 db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb create mode 100644 db/post_migrate/20210511051718_create_index_on_notes_note.rb create mode 100644 db/schema_migrations/20210430122951 create mode 100644 db/schema_migrations/20210430124212 create mode 100644 db/schema_migrations/20210430124630 create mode 100644 db/schema_migrations/20210430130259 create mode 100644 db/schema_migrations/20210430134202 create mode 100644 db/schema_migrations/20210430135954 create mode 100644 db/schema_migrations/20210511051718 delete mode 100644 doc/user/group/epics/img/epic_view_v13.0.png delete mode 100644 doc/user/group/epics/img/new_epic_from_groups_v13.7.png create mode 100644 lib/gitlab/database/with_lock_retries_outside_transaction.rb delete mode 100644 lib/gitlab/pages/stores/local_store.rb create mode 100644 qa/qa/resource/group_base.rb create mode 100644 spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb delete mode 100644 spec/lib/gitlab/pages/stores/local_store_spec.rb create mode 100644 spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb create mode 100644 spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb rename spec/models/{project_services => integrations}/chat_message/alert_message_spec.rb (96%) rename spec/models/{project_services => integrations}/chat_message/base_message_spec.rb (94%) rename spec/models/{project_services => integrations}/chat_message/deployment_message_spec.rb (98%) rename spec/models/{project_services => integrations}/chat_message/issue_message_spec.rb (98%) rename spec/models/{project_services => integrations}/chat_message/merge_message_spec.rb (98%) rename spec/models/{project_services => integrations}/chat_message/note_message_spec.rb (99%) rename spec/models/{project_services => integrations}/chat_message/pipeline_message_spec.rb (99%) rename spec/models/{project_services => integrations}/chat_message/push_message_spec.rb (99%) rename spec/models/{project_services => integrations}/chat_message/wiki_page_message_spec.rb (98%) diff --git a/.gitlab/ci/build-images.gitlab-ci.yml b/.gitlab/ci/build-images.gitlab-ci.yml index 4e352472047..ed1f71e27bb 100644 --- a/.gitlab/ci/build-images.gitlab-ci.yml +++ b/.gitlab/ci/build-images.gitlab-ci.yml @@ -9,8 +9,18 @@ build-qa-image: - .build-images:rules:build-qa-image stage: build-images needs: [] + variables: + QA_IMAGE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}" script: - - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}" + # With .git/hooks/post-checkout in place, Git tries to pull LFS objects, but the image doesn't have Git LFS, and we actually don't care about it for this specific so we just remove the file. + # Without removing the file, the error is as follows: "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout." + - rm .git/hooks/post-checkout + # Use $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA so that GitLab image built in omnibus-gitlab-mirror and QA image are in sync. + # This falls back to $CI_COMMIT_SHA (the default checked out commit) for the non-merged result pipelines. + # See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/index.html#with-pipeline-for-merged-results. + - if [ -n "$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA" ]; then + git checkout -f ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}; + fi - /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --cache=true retry: 2 diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 891457afe6e..0cadce5cc20 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -128,7 +128,7 @@ .use-kaniko: image: - name: gcr.io/kaniko-project/executor:debug-v1.3.0 + name: registry.gitlab.com/gitlab-org/gitlab-build-images:kaniko entrypoint: [""] before_script: - source scripts/utils.sh diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 778a39c9a6e..2ec3df1aeda 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -2941,7 +2941,6 @@ Style/RegexpLiteralMixedPreserve: - 'app/models/concerns/ci/maskable.rb' - 'app/models/operations/feature_flag.rb' - 'app/models/packages/go/module.rb' - - 'app/models/project_services/chat_message/base_message.rb' - 'app/services/packages/conan/search_service.rb' - 'app/services/projects/update_remote_mirror_service.rb' - 'config/initializers/rspec_profiling.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0f2f3bc44a4..26b12f26943 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -598,7 +598,6 @@ Rails/RenderInline: # SupportedStyles: conservative, aggressive Rails/ShortI18n: Exclude: - - 'app/models/project_services/chat_message/pipeline_message.rb' - 'app/uploaders/content_type_whitelist.rb' # Offense count: 1144 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index dcc93f19e1c..ffe37cdc2cf 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -2be01682b4a0ba12cad12b30bad4dc8876503f22 +c93530dd0922e7554d6d1e486e830f72980fe083 diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index 04d80dc0069..ad557f64ce4 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -333,7 +333,7 @@ export default { ghostClass: 'gl-visibility-hidden', }, i18n: { - dropzoneDescriptionText: __('Drop or %{linkStart}upload%{linkEnd} designs to attach'), + dropzoneDescriptionText: __('Drag your designs here or %{linkStart}click to upload%{linkEnd}.'), }, }; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue index 6764c5df06a..67242b3b5b7 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue @@ -14,6 +14,13 @@ export default { GlTable, }, inject: ['issuableId', 'issuableType'], + props: { + limitToHours: { + type: Boolean, + default: false, + required: false, + }, + }, data() { return { report: [], isLoading: true }; }, @@ -60,7 +67,10 @@ export default { }, formatTimeSpent(seconds) { const negative = seconds < 0; - return (negative ? '- ' : '') + stringifyTime(parseSeconds(seconds)); + return ( + (negative ? '- ' : '') + + stringifyTime(parseSeconds(seconds, { limitToHours: this.limitToHours })) + ); }, }, fields: [ diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index ac3d278d840..64f2ddc1d16 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -180,7 +180,7 @@ export default { :title="__('Time tracking report')" :hide-footer="true" > - + diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue index c2e879b45bc..86788a84260 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue @@ -178,7 +178,7 @@ export default { class="labels-fetch-loading gl-align-items-center w-100 h-100" size="md" /> -
    +
      { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } scope :has_version, -> { where.not(version: nil) } - scope :processed, -> do - where.not(package_type: :nuget).or( - where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) - ) - end scope :preload_files, -> { preload(:package_files) } scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) } scope :limit_recent, ->(limit) { order_created_desc.limit(limit) } diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index bd4b232849f..17131cd736d 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -62,17 +62,16 @@ module Pages } end + # TODO: remove support for legacy storage in 14.3 https://gitlab.com/gitlab-org/gitlab/-/issues/328712 + # we support this till 14.3 to allow people to still use legacy storage if something goes very wrong + # on self-hosted installations, and we'll need some time to fix it def legacy_source - raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true) + return unless ::Settings.pages.local_store.enabled { type: 'file', path: File.join(project.full_path, 'public/') } - rescue LegacyStorageDisabledError => e - Gitlab::ErrorTracking.track_exception(e, project_id: project.id) - - nil end end end diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb index 90cb8253b52..497f67993ae 100644 --- a/app/models/pages/virtual_domain.rb +++ b/app/models/pages/virtual_domain.rb @@ -21,9 +21,7 @@ module Pages project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain) end - # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524 - # source can only be nil if pages_serve_from_legacy_storage FF is disabled - # we can remove this filtering once we remove legacy storage + # TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/328715 paths = paths.select(&:source) paths.sort_by(&:prefix).reverse diff --git a/app/models/project_services/chat_message/alert_message.rb b/app/models/project_services/chat_message/alert_message.rb deleted file mode 100644 index c8913775843..00000000000 --- a/app/models/project_services/chat_message/alert_message.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class AlertMessage < BaseMessage - attr_reader :title - attr_reader :alert_url - attr_reader :severity - attr_reader :events - attr_reader :status - attr_reader :started_at - - def initialize(params) - @project_name = params[:project_name] || params.dig(:project, :path_with_namespace) - @project_url = params.dig(:project, :web_url) || params[:project_url] - @title = params.dig(:object_attributes, :title) - @alert_url = params.dig(:object_attributes, :url) - @severity = params.dig(:object_attributes, :severity) - @events = params.dig(:object_attributes, :events) - @status = params.dig(:object_attributes, :status) - @started_at = params.dig(:object_attributes, :started_at) - end - - def attachments - [{ - title: title, - title_link: alert_url, - color: attachment_color, - fields: attachment_fields - }] - end - - def message - "Alert firing in #{project_name}" - end - - private - - def attachment_color - "#C95823" - end - - def attachment_fields - [ - { - title: "Severity", - value: severity.to_s.humanize, - short: true - }, - { - title: "Events", - value: events, - short: true - }, - { - title: "Status", - value: status.to_s.humanize, - short: true - }, - { - title: "Start time", - value: format_time(started_at), - short: true - } - ] - end - - # This formats time into the following format - # April 23rd, 2020 1:06AM UTC - def format_time(time) - time = Time.zone.parse(time.to_s) - time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z") - end - end -end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb deleted file mode 100644 index bdd77a919e3..00000000000 --- a/app/models/project_services/chat_message/base_message.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class BaseMessage - RELATIVE_LINK_REGEX = /!\[[^\]]*\]\((\/uploads\/[^\)]*)\)/.freeze - - attr_reader :markdown - attr_reader :user_full_name - attr_reader :user_name - attr_reader :user_avatar - attr_reader :project_name - attr_reader :project_url - - def initialize(params) - @markdown = params[:markdown] || false - @project_name = params[:project_name] || params.dig(:project, :path_with_namespace) - @project_url = params.dig(:project, :web_url) || params[:project_url] - @user_full_name = params.dig(:user, :name) || params[:user_full_name] - @user_name = params.dig(:user, :username) || params[:user_name] - @user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar] - end - - def user_combined_name - if user_full_name.present? - "#{user_full_name} (#{user_name})" - else - user_name - end - end - - def summary - return message if markdown - - format(message) - end - - def pretext - summary - end - - def fallback - format(message) - end - - def attachments - raise NotImplementedError - end - - def activity - raise NotImplementedError - end - - private - - def message - raise NotImplementedError - end - - def format(string) - Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string)) - end - - def format_relative_links(string) - string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1") - end - - def attachment_color - '#345' - end - - def link(text, url) - "[#{text}](#{url})" - end - - def pretty_duration(seconds) - parse_string = - if duration < 1.hour - '%M:%S' - else - '%H:%M:%S' - end - - Time.at(seconds).utc.strftime(parse_string) - end - end -end diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb deleted file mode 100644 index 5deb757e60f..00000000000 --- a/app/models/project_services/chat_message/deployment_message.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class DeploymentMessage < BaseMessage - attr_reader :commit_title - attr_reader :commit_url - attr_reader :deployable_id - attr_reader :deployable_url - attr_reader :environment - attr_reader :short_sha - attr_reader :status - attr_reader :user_url - - def initialize(data) - super - - @commit_title = data[:commit_title] - @commit_url = data[:commit_url] - @deployable_id = data[:deployable_id] - @deployable_url = data[:deployable_url] - @environment = data[:environment] - @short_sha = data[:short_sha] - @status = data[:status] - @user_url = data[:user_url] - end - - def attachments - [{ - text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}", - color: color - }] - end - - def activity - {} - end - - private - - def message - if running? - "Starting deploy to #{environment}" - else - "Deploy to #{environment} #{humanized_status}" - end - end - - def color - case status - when 'success' - 'good' - when 'canceled' - 'warning' - when 'failed' - 'danger' - else - '#334455' - end - end - - def project_link - link(project_name, project_url) - end - - def deployment_link - link("##{deployable_id}", deployable_url) - end - - def user_link - link(user_combined_name, user_url) - end - - def commit_link - link(short_sha, commit_url) - end - - def humanized_status - status == 'success' ? 'succeeded' : status - end - - def running? - status == 'running' - end - end -end diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb deleted file mode 100644 index c8e90b66bae..00000000000 --- a/app/models/project_services/chat_message/issue_message.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class IssueMessage < BaseMessage - attr_reader :title - attr_reader :issue_iid - attr_reader :issue_url - attr_reader :action - attr_reader :state - attr_reader :description - - def initialize(params) - super - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @title = obj_attr[:title] - @issue_iid = obj_attr[:iid] - @issue_url = obj_attr[:url] - @action = obj_attr[:action] - @state = obj_attr[:state] - @description = obj_attr[:description] || '' - end - - def attachments - return [] unless opened_issue? - return description if markdown - - description_message - end - - def activity - { - title: "Issue #{state} by #{user_combined_name}", - subtitle: "in #{project_link}", - text: issue_link, - image: user_avatar - } - end - - private - - def message - "[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}" - end - - def opened_issue? - action == 'open' - end - - def description_message - [{ - title: issue_title, - title_link: issue_url, - text: format(description), - color: '#C95823' - }] - end - - def project_link - link(project_name, project_url) - end - - def issue_link - link(issue_title, issue_url) - end - - def issue_title - "#{Issue.reference_prefix}#{issue_iid} #{title}" - end - end -end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb deleted file mode 100644 index e45bb9b8ce1..00000000000 --- a/app/models/project_services/chat_message/merge_message.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class MergeMessage < BaseMessage - attr_reader :merge_request_iid - attr_reader :source_branch - attr_reader :target_branch - attr_reader :action - attr_reader :state - attr_reader :title - - def initialize(params) - super - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @merge_request_iid = obj_attr[:iid] - @source_branch = obj_attr[:source_branch] - @target_branch = obj_attr[:target_branch] - @action = obj_attr[:action] - @state = obj_attr[:state] - @title = format_title(obj_attr[:title]) - end - - def attachments - [] - end - - def activity - { - title: "Merge request #{state_or_action_text} by #{user_combined_name}", - subtitle: "in #{project_link}", - text: merge_request_link, - image: user_avatar - } - end - - private - - def format_title(title) - '*' + title.lines.first.chomp + '*' - end - - def message - merge_request_message - end - - def project_link - link(project_name, project_url) - end - - def merge_request_message - "#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}" - end - - def merge_request_link - link(merge_request_title, merge_request_url) - end - - def merge_request_title - "#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}" - end - - def merge_request_url - "#{project_url}/-/merge_requests/#{merge_request_iid}" - end - - def state_or_action_text - case action - when 'approved', 'unapproved' - action - when 'approval' - 'added their approval to' - when 'unapproval' - 'removed their approval from' - else - state - end - end - end -end diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb deleted file mode 100644 index 741474fb27b..00000000000 --- a/app/models/project_services/chat_message/note_message.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class NoteMessage < BaseMessage - attr_reader :note - attr_reader :note_url - attr_reader :title - attr_reader :target - - def initialize(params) - super - - params = HashWithIndifferentAccess.new(params) - obj_attr = params[:object_attributes] - @note = obj_attr[:note] - @note_url = obj_attr[:url] - @target, @title = case obj_attr[:noteable_type] - when "Commit" - create_commit_note(params[:commit]) - when "Issue" - create_issue_note(params[:issue]) - when "MergeRequest" - create_merge_note(params[:merge_request]) - when "Snippet" - create_snippet_note(params[:snippet]) - end - end - - def attachments - return note if markdown - - description_message - end - - def activity - { - title: "#{user_combined_name} #{link('commented on ' + target, note_url)}", - subtitle: "in #{project_link}", - text: formatted_title, - image: user_avatar - } - end - - private - - def message - "#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" - end - - def format_title(title) - title.lines.first.chomp - end - - def formatted_title - format_title(title) - end - - def create_issue_note(issue) - ["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]] - end - - def create_commit_note(commit) - commit_sha = Commit.truncate_sha(commit[:id]) - - ["commit #{commit_sha}", commit[:message]] - end - - def create_merge_note(merge_request) - ["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]] - end - - def create_snippet_note(snippet) - ["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]] - end - - def description_message - [{ text: format(note), color: attachment_color }] - end - - def project_link - link(project_name, project_url) - end - end -end diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb deleted file mode 100644 index f4c6938fa78..00000000000 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ /dev/null @@ -1,265 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class PipelineMessage < BaseMessage - MAX_VISIBLE_JOBS = 10 - - attr_reader :user - attr_reader :ref_type - attr_reader :ref - attr_reader :status - attr_reader :detailed_status - attr_reader :duration - attr_reader :finished_at - attr_reader :pipeline_id - attr_reader :failed_stages - attr_reader :failed_jobs - - attr_reader :project - attr_reader :commit - attr_reader :committer - attr_reader :pipeline - - def initialize(data) - super - - @user = data[:user] - @user_name = data.dig(:user, :username) || 'API' - - pipeline_attributes = data[:object_attributes] - @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' - @ref = pipeline_attributes[:ref] - @status = pipeline_attributes[:status] - @detailed_status = pipeline_attributes[:detailed_status] - @duration = pipeline_attributes[:duration].to_i - @finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil - @pipeline_id = pipeline_attributes[:id] - - # Get list of jobs that have actually failed (after exhausting all retries) - @failed_jobs = actually_failed_jobs(Array(data[:builds])) - @failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq - - @project = Project.find(data[:project][:id]) - @commit = project.commit_by(oid: data[:commit][:id]) - @committer = commit.committer - @pipeline = Ci::Pipeline.find(pipeline_id) - end - - def pretext - '' - end - - def attachments - return message if markdown - - [{ - fallback: format(message), - color: attachment_color, - author_name: user_combined_name, - author_icon: user_avatar, - author_link: author_url, - title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") % - { - pipeline_id: pipeline_id, - humanized_status: humanized_status, - duration: pretty_duration(duration) - }, - title_link: pipeline_url, - fields: attachments_fields, - footer: project.name, - footer_icon: project.avatar_url(only_path: false), - ts: finished_at - }] - end - - def activity - { - title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") % - { - pipeline_link: pipeline_link, - ref_type: ref_type, - ref_link: ref_link, - user_combined_name: user_combined_name, - humanized_status: humanized_status - }, - subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link }, - text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) }, - image: user_avatar || '' - } - end - - private - - def actually_failed_jobs(builds) - succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq - - failed_jobs = builds.select do |build| - # Select jobs which doesn't have a successful retry - build[:status] == 'failed' && !succeeded_job_names.include?(build[:name]) - end - - failed_jobs.uniq { |job| job[:name] }.reverse - end - - def failed_stages_field - { - title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length), - value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links), - short: true - } - end - - def failed_jobs_field - { - title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length), - value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links), - short: true - } - end - - def yaml_error_field - { - title: s_("ChatMessage|Invalid CI config YAML file"), - value: pipeline.yaml_errors, - short: false - } - end - - def attachments_fields - fields = [ - { - title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"), - value: Slack::Messenger::Util::LinkFormatter.format(ref_link), - short: true - }, - { - title: s_("ChatMessage|Commit"), - value: Slack::Messenger::Util::LinkFormatter.format(commit_link), - short: true - } - ] - - fields << failed_stages_field if failed_stages.any? - fields << failed_jobs_field if failed_jobs.any? - fields << yaml_error_field if pipeline.has_yaml_errors? - - fields - end - - def message - s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") % - { - project_link: project_link, - pipeline_link: pipeline_link, - ref_type: ref_type, - ref_link: ref_link, - user_combined_name: user_combined_name, - humanized_status: humanized_status, - duration: pretty_duration(duration) - } - end - - def humanized_status - case status - when 'success' - detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed") - when 'failed' - s_("ChatMessage|has failed") - else - status - end - end - - def attachment_color - case status - when 'success' - detailed_status == 'passed with warnings' ? 'warning' : 'good' - else - 'danger' - end - end - - def ref_url - if ref_type == 'tag' - "#{project_url}/-/tags/#{ref}" - else - "#{project_url}/-/commits/#{ref}" - end - end - - def ref_link - "[#{ref}](#{ref_url})" - end - - def project_url - project.web_url - end - - def project_link - "[#{project.name}](#{project_url})" - end - - def pipeline_failed_jobs_url - "#{project_url}/-/pipelines/#{pipeline_id}/failures" - end - - def pipeline_url - if failed_jobs.any? - pipeline_failed_jobs_url - else - "#{project_url}/-/pipelines/#{pipeline_id}" - end - end - - def pipeline_link - "[##{pipeline_id}](#{pipeline_url})" - end - - def job_url(job) - "#{project_url}/-/jobs/#{job[:id]}" - end - - def job_link(job) - "[#{job[:name]}](#{job_url(job)})" - end - - def failed_jobs_links - failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS) - truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size) - - failed_links = failed.map { |job| job_link(job) } - - unless truncated.blank? - failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % { - count: truncated.size, - pipeline_failed_jobs_url: pipeline_failed_jobs_url - } - end - - failed_links.join(I18n.translate(:'support.array.words_connector')) - end - - def stage_link(stage) - # All stages link to the pipeline page - "[#{stage}](#{pipeline_url})" - end - - def failed_stages_links - failed_stages.map { |s| stage_link(s) }.join(I18n.translate(:'support.array.words_connector')) - end - - def commit_url - Gitlab::UrlBuilder.build(commit) - end - - def commit_link - "[#{commit.title}](#{commit_url})" - end - - def author_url - return unless user && committer - - Gitlab::UrlBuilder.build(committer) - end - end -end diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb deleted file mode 100644 index c8e70a69c88..00000000000 --- a/app/models/project_services/chat_message/push_message.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class PushMessage < BaseMessage - attr_reader :after - attr_reader :before - attr_reader :commits - attr_reader :ref - attr_reader :ref_type - - def initialize(params) - super - - @after = params[:after] - @before = params[:before] - @commits = params.fetch(:commits, []) - @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' - @ref = Gitlab::Git.ref_name(params[:ref]) - end - - def attachments - return [] if new_branch? || removed_branch? - return commit_messages if markdown - - commit_message_attachments - end - - def activity - { - title: humanized_action(short: true), - subtitle: "in #{project_link}", - text: compare_link, - image: user_avatar - } - end - - private - - def humanized_action(short: false) - action, ref_link, target_link = compose_action_details - text = [user_combined_name, action, ref_type, ref_link] - text << target_link unless short - text.join(' ') - end - - def message - humanized_action - end - - def format(string) - Slack::Messenger::Util::LinkFormatter.format(string) - end - - def commit_messages - commits.map { |commit| compose_commit_message(commit) }.join("\n\n") - end - - def commit_message_attachments - [{ text: format(commit_messages), color: attachment_color }] - end - - def compose_commit_message(commit) - author = commit[:author][:name] - id = Commit.truncate_sha(commit[:id]) - title = commit[:title] - - url = commit[:url] - - "[#{id}](#{url}): #{title} - #{author}" - end - - def new_branch? - Gitlab::Git.blank_ref?(before) - end - - def removed_branch? - Gitlab::Git.blank_ref?(after) - end - - def ref_url - if ref_type == 'tag' - "#{project_url}/-/tags/#{ref}" - else - "#{project_url}/commits/#{ref}" - end - end - - def compare_url - "#{project_url}/compare/#{before}...#{after}" - end - - def ref_link - "[#{ref}](#{ref_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def compare_link - "[Compare changes](#{compare_url})" - end - - def compose_action_details - if new_branch? - ['pushed new', ref_link, "to #{project_link}"] - elsif removed_branch? - ['removed', ref, "from #{project_link}"] - else - ['pushed to', ref_link, "of #{project_link} (#{compare_link})"] - end - end - - def attachment_color - '#345' - end - end -end diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb deleted file mode 100644 index ebe7abb379f..00000000000 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -module ChatMessage - class WikiPageMessage < BaseMessage - attr_reader :title - attr_reader :wiki_page_url - attr_reader :action - attr_reader :description - - def initialize(params) - super - - obj_attr = params[:object_attributes] - obj_attr = HashWithIndifferentAccess.new(obj_attr) - @title = obj_attr[:title] - @wiki_page_url = obj_attr[:url] - @description = obj_attr[:message] - - @action = - case obj_attr[:action] - when "create" - "created" - when "update" - "edited" - end - end - - def attachments - return description if markdown - - description_message - end - - def activity - { - title: "#{user_combined_name} #{action} #{wiki_page_link}", - subtitle: "in #{project_link}", - text: title, - image: user_avatar - } - end - - private - - def message - "#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*" - end - - def description_message - [{ text: format(@description), color: attachment_color }] - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def wiki_page_link - "[wiki page](#{wiki_page_url})" - end - end -end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index c95a5d4e6cb..2f841bf903e 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -185,19 +185,19 @@ class ChatNotificationService < Integration def get_message(object_kind, data) case object_kind when "push", "tag_push" - ChatMessage::PushMessage.new(data) if notify_for_ref?(data) + Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data) when "issue" - ChatMessage::IssueMessage.new(data) unless update?(data) + Integrations::ChatMessage::IssueMessage.new(data) unless update?(data) when "merge_request" - ChatMessage::MergeMessage.new(data) unless update?(data) + Integrations::ChatMessage::MergeMessage.new(data) unless update?(data) when "note" - ChatMessage::NoteMessage.new(data) + Integrations::ChatMessage::NoteMessage.new(data) when "pipeline" - ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) + Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) when "wiki_page" - ChatMessage::WikiPageMessage.new(data) + Integrations::ChatMessage::WikiPageMessage.new(data) when "deployment" - ChatMessage::DeploymentMessage.new(data) + Integrations::ChatMessage::DeploymentMessage.new(data) end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 7badcc24870..92a46f8d01f 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -39,7 +39,7 @@ class SlackService < ChatNotificationService end def get_message(object_kind, data) - return ChatMessage::AlertMessage.new(data) if object_kind == 'alert' + return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert' super end diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb index 8b5a94c021c..b75905fb5b0 100644 --- a/app/services/issuable/destroy_service.rb +++ b/app/services/issuable/destroy_service.rb @@ -26,25 +26,13 @@ module Issuable end def delete_todos(actor, issuable) - if Feature.enabled?(:destroy_issuable_todos_async, actor, default_enabled: :yaml) - TodosDestroyer::DestroyedIssuableWorker - .perform_async(issuable.id, issuable.class.name) - else - TodosDestroyer::DestroyedIssuableWorker - .new - .perform(issuable.id, issuable.class.name) - end + TodosDestroyer::DestroyedIssuableWorker + .perform_async(issuable.id, issuable.class.name) end def delete_label_links(actor, issuable) - if Feature.enabled?(:destroy_issuable_label_links_async, actor, default_enabled: :yaml) - Issuable::LabelLinksDestroyWorker - .perform_async(issuable.id, issuable.class.name) - else - Issuable::LabelLinksDestroyWorker - .new - .perform(issuable.id, issuable.class.name) - end + Issuable::LabelLinksDestroyWorker + .perform_async(issuable.id, issuable.class.name) end end end diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 32e58fcc06b..7155017b73f 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -16,12 +16,17 @@ module Spam def execute spamcheck_result = nil spamcheck_attribs = {} + spamcheck_error = false external_spam_check_round_trip_time = Benchmark.realtime do - spamcheck_result, spamcheck_attribs = spamcheck_verdict + spamcheck_result, spamcheck_attribs, spamcheck_error = spamcheck_verdict end - # assign result to a var and log it before reassigning to nil when monitorMode is true + label = spamcheck_error ? 'ERROR' : spamcheck_result.to_s.upcase + + histogram.observe( { result: label }, external_spam_check_round_trip_time ) + + # assign result to a var for logging it before reassigning to nil when monitorMode is true original_spamcheck_result = spamcheck_result spamcheck_result = nil if spamcheck_attribs&.fetch("monitorMode", "false") == "true" @@ -83,8 +88,9 @@ module Spam end rescue StandardError => e Gitlab::ErrorTracking.log_exception(e) + # Default to ALLOW if any errors occur - [ALLOW, attribs] + [ALLOW, attribs, true] end end @@ -95,5 +101,9 @@ module Spam def logger @logger ||= Gitlab::AppJsonLogger.build end + + def histogram + @histogram ||= Gitlab::Metrics.histogram(:gitlab_spamcheck_request_duration_seconds, 'Request duration to the anti-spam service') + end end end diff --git a/app/views/notify/change_in_merge_request_draft_status_email.html.haml b/app/views/notify/change_in_merge_request_draft_status_email.html.haml index 5604a30d9f1..64ceb77e85c 100644 --- a/app/views/notify/change_in_merge_request_draft_status_email.html.haml +++ b/app/views/notify/change_in_merge_request_draft_status_email.html.haml @@ -1,2 +1,2 @@ -%p - = _('%{username} changed the draft status of merge request %{mr_reference}' % {username: sanitize_name(@updated_by_user.name), mr_reference: @merge_request.to_reference }) +%p= html_escape(_('%{username} changed the draft status of merge request %{mr_link}')) % { username: link_to(@updated_by_user.name, user_url(@updated_by_user)), + mr_link: merge_request_reference_link(@merge_request) } diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 6c3e15cbace..01ab7bf9cd4 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -6,7 +6,7 @@ - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - if issuable_mr > 0 - %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests') } + %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests'), data: { testid: 'merge-requests' } } = sprite_icon('merge-request', css_class: "gl-vertical-align-middle") = issuable_mr diff --git a/changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml b/changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml new file mode 100644 index 00000000000..b8acba79b42 --- /dev/null +++ b/changelogs/unreleased/283891-fix-inconsistent-drag-and-drop-message-in-markdown-and-designs.yml @@ -0,0 +1,5 @@ +--- +title: Change wording for design management upload +merge_request: 61782 +author: +type: other diff --git a/changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml b/changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml new file mode 100644 index 00000000000..678b65a15c7 --- /dev/null +++ b/changelogs/unreleased/296138-disable-pages_update_legacy_storage-feature-flag-on-gitlab-com.yml @@ -0,0 +1,5 @@ +--- +title: Remove pages_update_legacy_storage feature flag +merge_request: 60005 +author: +type: added diff --git a/changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml b/changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml new file mode 100644 index 00000000000..44959cfb408 --- /dev/null +++ b/changelogs/unreleased/297524-disable-serving-pages-from-legacy-storage-2.yml @@ -0,0 +1,5 @@ +--- +title: Remove pages_serve_from_legacy_storage feature flag +merge_request: 60010 +author: +type: added diff --git a/changelogs/unreleased/325689-remove-delete-async-ffs.yml b/changelogs/unreleased/325689-remove-delete-async-ffs.yml new file mode 100644 index 00000000000..fcccf464740 --- /dev/null +++ b/changelogs/unreleased/325689-remove-delete-async-ffs.yml @@ -0,0 +1,5 @@ +--- +title: Remove issuable destroy service related FFs +merge_request: 61764 +author: +type: other diff --git a/changelogs/unreleased/329521-store-segment-target-groups.yml b/changelogs/unreleased/329521-store-segment-target-groups.yml new file mode 100644 index 00000000000..d59585c5fce --- /dev/null +++ b/changelogs/unreleased/329521-store-segment-target-groups.yml @@ -0,0 +1,5 @@ +--- +title: Prepare devops adoption database structure for migration +merge_request: 60733 +author: +type: other diff --git a/changelogs/unreleased/330845-observe-limit-to-hours.yml b/changelogs/unreleased/330845-observe-limit-to-hours.yml new file mode 100644 index 00000000000..c6108df10f0 --- /dev/null +++ b/changelogs/unreleased/330845-observe-limit-to-hours.yml @@ -0,0 +1,5 @@ +--- +title: Observe limit to hours setting in timelog report +merge_request: 61849 +author: Lee Tickett @leetickett +type: added diff --git a/changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml b/changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml new file mode 100644 index 00000000000..06d4c086735 --- /dev/null +++ b/changelogs/unreleased/add_duplicate_notes_note_trigram_index.yml @@ -0,0 +1,5 @@ +--- +title: Add duplicated gin trigram index on notes table (note) to replace existing +merge_request: 61430 +author: +type: performance diff --git a/changelogs/unreleased/cablett-change-draft-status-email.yml b/changelogs/unreleased/cablett-change-draft-status-email.yml new file mode 100644 index 00000000000..5d364a240c6 --- /dev/null +++ b/changelogs/unreleased/cablett-change-draft-status-email.yml @@ -0,0 +1,5 @@ +--- +title: Add link to email notifying of MR changing draft status +merge_request: 61891 +author: +type: changed diff --git a/changelogs/unreleased/grape-action-caching-default.yml b/changelogs/unreleased/grape-action-caching-default.yml new file mode 100644 index 00000000000..de46a8e75f6 --- /dev/null +++ b/changelogs/unreleased/grape-action-caching-default.yml @@ -0,0 +1,5 @@ +--- +title: Apply rate-limit cache to branches endpoint +merge_request: 61723 +author: +type: performance diff --git a/changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml b/changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml new file mode 100644 index 00000000000..e2300a390e2 --- /dev/null +++ b/changelogs/unreleased/nicolasdular-email-campaign-usage-data.yml @@ -0,0 +1,5 @@ +--- +title: Send in-product marketing email usage data +merge_request: 56752 +author: +type: changed diff --git a/config/feature_flags/development/api_caching_rate_limit_branches.yml b/config/feature_flags/development/api_caching_rate_limit_branches.yml index a4feb9fc8a0..a48e4660342 100644 --- a/config/feature_flags/development/api_caching_rate_limit_branches.yml +++ b/config/feature_flags/development/api_caching_rate_limit_branches.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330876 milestone: '13.12' type: development group: group::source code -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/destroy_issuable_label_links_async.yml b/config/feature_flags/development/destroy_issuable_label_links_async.yml deleted file mode 100644 index 14abd9459f9..00000000000 --- a/config/feature_flags/development/destroy_issuable_label_links_async.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: destroy_issuable_label_links_async -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60487 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325689 -milestone: '13.12' -type: development -group: group::code review -default_enabled: false diff --git a/config/feature_flags/development/destroy_issuable_todos_async.yml b/config/feature_flags/development/destroy_issuable_todos_async.yml deleted file mode 100644 index c39e551bdd9..00000000000 --- a/config/feature_flags/development/destroy_issuable_todos_async.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: destroy_issuable_todos_async -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57830 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325689 -milestone: '13.11' -type: development -group: group::code review -default_enabled: false diff --git a/config/feature_flags/development/pages_serve_from_legacy_storage.yml b/config/feature_flags/development/pages_serve_from_legacy_storage.yml deleted file mode 100644 index 37d83106737..00000000000 --- a/config/feature_flags/development/pages_serve_from_legacy_storage.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: pages_serve_from_legacy_storage -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297228 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297524 -milestone: '13.8' -type: development -group: group::release -default_enabled: true diff --git a/config/feature_flags/development/pages_update_legacy_storage.yml b/config/feature_flags/development/pages_update_legacy_storage.yml deleted file mode 100644 index 4a228b4cb8b..00000000000 --- a/config/feature_flags/development/pages_update_legacy_storage.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: pages_update_legacy_storage -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50683 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296138 -milestone: '13.9' -type: development -group: group::release -default_enabled: true diff --git a/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml b/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml new file mode 100644 index 00000000000..bbfae3eb114 --- /dev/null +++ b/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_create_0_sent +name: "count_sent_first_email_of_the_create_track_for_in_product_marketing_emails" +description: Total sent emails of the create track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml b/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml new file mode 100644 index 00000000000..a2cf7c6d813 --- /dev/null +++ b/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.in_product_marketing_email_create_0_cta_clicked +name: "count_clicks_on_the_first_email_of_the_create_track_for_in_product_marketing_emails" +description: Total clicks on the create track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate + diff --git a/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml b/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml new file mode 100644 index 00000000000..85552f42431 --- /dev/null +++ b/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_create_1_sent +name: "count_sent_second_email_of_the_create_track_for_in_product_marketing_emails" +description: Total sent emails of the create track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml b/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml new file mode 100644 index 00000000000..7a1e5f28c22 --- /dev/null +++ b/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.in_product_marketing_email_create_1_cta_clicked +name: "count_clicks_on_the_second_email_of_the_create_track_for_in_product_marketing_emails" +description: Total clicks on the create track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate + diff --git a/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml b/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml new file mode 100644 index 00000000000..02d5ae115bb --- /dev/null +++ b/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_create_2_sent +name: "count_sent_third_email_of_the_create_track_for_in_product_marketing_emails" +description: Total sent emails of the create track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml b/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml new file mode 100644 index 00000000000..75eafa3a54f --- /dev/null +++ b/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_create_2_cta_clicked +name: "count_clicks_on_the_third_email_of_the_create_track_for_in_product_marketing_emails" +description: Total clicks on the create track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml b/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml new file mode 100644 index 00000000000..0a5a9ef936f --- /dev/null +++ b/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_verify_0_sent +name: "count_sent_first_email_of_the_verify_track_for_in_product_marketing_emails" +description: Total sent emails of the verify track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml b/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml new file mode 100644 index 00000000000..17e677ba29d --- /dev/null +++ b/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_verify_0_cta_clicked +name: "count_clicks_on_the_first_email_of_the_verify_track_for_in_product_marketing_emails" +description: Total clicks on the verify track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml b/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml new file mode 100644 index 00000000000..2bad6d31db2 --- /dev/null +++ b/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_verify_1_sent +name: "count_sent_second_email_of_the_verify_track_for_in_product_marketing_emails" +description: Total sent emails of the verify track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml b/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml new file mode 100644 index 00000000000..66080d46e7f --- /dev/null +++ b/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.in_product_marketing_email_verify_1_cta_clicked +name: "count_clicks_on_the_second_email_of_the_verify_track_for_in_product_marketing_emails" +description: Total clicks on the verify track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate + diff --git a/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml b/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml new file mode 100644 index 00000000000..2beabb8b007 --- /dev/null +++ b/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_verify_2_sent +name: "count_sent_third_email_of_the_verify_track_for_in_product_marketing_emails" +description: Total sent emails of the verify track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml b/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml new file mode 100644 index 00000000000..7213c463fa9 --- /dev/null +++ b/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_verify_2_cta_clicked +name: "count_clicks_on_the_third_email_of_the_verify_track_for_in_product_marketing_emails" +description: Total clicks on the verify track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml b/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml new file mode 100644 index 00000000000..dc566f03898 --- /dev/null +++ b/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_trial_0_sent +name: "count_sent_first_email_of_the_trial_track_for_in_product_marketing_emails" +description: Total sent emails of the trial track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml b/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml new file mode 100644 index 00000000000..a3cf714e5ad --- /dev/null +++ b/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_trial_0_cta_clicked +name: "count_clicks_on_the_first_email_of_the_trial_track_for_in_product_marketing_emails" +description: Total clicks on the verify trial's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml b/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml new file mode 100644 index 00000000000..f5215090b7e --- /dev/null +++ b/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_trial_1_sent +name: "count_sent_second_email_of_the_trial_track_for_in_product_marketing_emails" +description: Total sent emails of the trial track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml b/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml new file mode 100644 index 00000000000..651c5426e2a --- /dev/null +++ b/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.in_product_marketing_email_trial_1_cta_clicked +name: "count_clicks_on_the_second_email_of_the_trial_track_for_in_product_marketing_emails" +description: Total clicks on the trial track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate + diff --git a/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml b/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml new file mode 100644 index 00000000000..4be98d45ce2 --- /dev/null +++ b/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_trial_2_sent +name: "count_sent_third_email_of_the_trial_track_for_in_product_marketing_emails" +description: Total sent emails of the trial track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml b/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml new file mode 100644 index 00000000000..6be928da906 --- /dev/null +++ b/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_trial_2_cta_clicked +name: "count_clicks_on_the_third_email_of_the_trial_track_for_in_product_marketing_emails" +description: Total clicks on the trial track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml b/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml new file mode 100644 index 00000000000..ac9ffa730f1 --- /dev/null +++ b/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_team_0_sent +name: "count_sent_first_email_of_the_trial_team_for_in_product_marketing_emails" +description: Total sent emails of the team track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml b/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml new file mode 100644 index 00000000000..cf51512c6eb --- /dev/null +++ b/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_team_0_cta_clicked +name: "count_clicks_on_the_first_email_of_the_team_track_for_in_product_marketing_emails" +description: Total clicks on the team track's first email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml b/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml new file mode 100644 index 00000000000..b860b08e391 --- /dev/null +++ b/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_team_1_sent +name: "count_sent_second_email_of_the_team_track_for_in_product_marketing_emails" +description: Total sent emails of the team track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml b/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml new file mode 100644 index 00000000000..c0f63cadbf2 --- /dev/null +++ b/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.in_product_marketing_email_team_1_cta_clicked +name: "count_clicks_on_the_second_email_of_the_team_track_for_in_product_marketing_emails" +description: Total clicks on the team track's second email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate + diff --git a/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml b/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml new file mode 100644 index 00000000000..887334c65c7 --- /dev/null +++ b/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_team_2_sent +name: "count_sent_third_email_of_the_team_track_for_in_product_marketing_emails" +description: Total sent emails of the team track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml b/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml new file mode 100644 index 00000000000..6dac2db454a --- /dev/null +++ b/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml @@ -0,0 +1,21 @@ +--- +key_path: counts.in_product_marketing_email_team_2_cta_clicked +name: "count_clicks_on_the_third_email_of_the_team_track_for_in_product_marketing_emails" +description: Total clicks on the team track's third email +product_section: +product_stage: growth +product_group: group::activation +product_category: onboarding +value_type: number +status: implemented +milestone: "13.12" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56752 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/db/migrate/20210430122951_add_snapshot_namespace_id.rb b/db/migrate/20210430122951_add_snapshot_namespace_id.rb new file mode 100644 index 00000000000..9017bcdde53 --- /dev/null +++ b/db/migrate/20210430122951_add_snapshot_namespace_id.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSnapshotNamespaceId < ActiveRecord::Migration[6.0] + def change + add_column :analytics_devops_adoption_snapshots, :namespace_id, :integer + end +end diff --git a/db/migrate/20210430124212_add_display_namespace_id_to_segments.rb b/db/migrate/20210430124212_add_display_namespace_id_to_segments.rb new file mode 100644 index 00000000000..43be5c719fb --- /dev/null +++ b/db/migrate/20210430124212_add_display_namespace_id_to_segments.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDisplayNamespaceIdToSegments < ActiveRecord::Migration[6.0] + def change + add_column :analytics_devops_adoption_segments, :display_namespace_id, :integer + end +end diff --git a/db/migrate/20210430124630_add_devops_adoption_indexes.rb b/db/migrate/20210430124630_add_devops_adoption_indexes.rb new file mode 100644 index 00000000000..4531e6b5e4c --- /dev/null +++ b/db/migrate/20210430124630_add_devops_adoption_indexes.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class AddDevopsAdoptionIndexes < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + SEGMENTS_INDEX_NAME = 'idx_devops_adoption_segments_namespaces_pair' + SNAPSHOT_END_TIME_INDEX_NAME = 'idx_devops_adoption_segments_namespace_end_time' + SNAPSHOT_RECORDED_AT_INDEX_NAME = 'idx_devops_adoption_segments_namespace_recorded_at' + + def up + add_concurrent_index :analytics_devops_adoption_snapshots, [:namespace_id, :end_time], + name: SNAPSHOT_END_TIME_INDEX_NAME + add_concurrent_index :analytics_devops_adoption_snapshots, [:namespace_id, :recorded_at], + name: SNAPSHOT_RECORDED_AT_INDEX_NAME + add_concurrent_index :analytics_devops_adoption_segments, [:display_namespace_id, :namespace_id], + unique: true, name: SEGMENTS_INDEX_NAME + + add_concurrent_foreign_key :analytics_devops_adoption_snapshots, :namespaces, column: :namespace_id + add_concurrent_foreign_key :analytics_devops_adoption_segments, :namespaces, column: :display_namespace_id + end + + def down + remove_foreign_key :analytics_devops_adoption_segments, :namespaces, column: :display_namespace_id + remove_foreign_key :analytics_devops_adoption_snapshots, :namespaces, column: :namespace_id + + remove_concurrent_index_by_name :analytics_devops_adoption_segments, SEGMENTS_INDEX_NAME + remove_concurrent_index_by_name :analytics_devops_adoption_snapshots, SNAPSHOT_RECORDED_AT_INDEX_NAME + remove_concurrent_index_by_name :analytics_devops_adoption_snapshots, SNAPSHOT_END_TIME_INDEX_NAME + end +end diff --git a/db/post_migrate/20210430130259_remove_obsolete_segments_field.rb b/db/post_migrate/20210430130259_remove_obsolete_segments_field.rb new file mode 100644 index 00000000000..ffdd84582cb --- /dev/null +++ b/db/post_migrate/20210430130259_remove_obsolete_segments_field.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemoveObsoleteSegmentsField < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + with_lock_retries do + remove_column :analytics_devops_adoption_segments, :name + end + end + + def down + add_column :analytics_devops_adoption_segments, :name, :text + add_text_limit :analytics_devops_adoption_segments, :name, 255 + end +end diff --git a/db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb b/db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb new file mode 100644 index 00000000000..d0a72ff2c43 --- /dev/null +++ b/db/post_migrate/20210430134202_copy_adoption_snapshot_namespace.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CopyAdoptionSnapshotNamespace < ActiveRecord::Migration[6.0] + def up + execute <<-SQL + UPDATE analytics_devops_adoption_snapshots snapshots + SET namespace_id = segments.namespace_id + FROM analytics_devops_adoption_segments segments + WHERE snapshots.namespace_id IS NULL AND segments.id = snapshots.segment_id + SQL + end + + def down + execute 'UPDATE analytics_devops_adoption_snapshots SET namespace_id = NULL' + end +end diff --git a/db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb b/db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb new file mode 100644 index 00000000000..04f454bea37 --- /dev/null +++ b/db/post_migrate/20210430135954_copy_adoption_segments_namespace.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CopyAdoptionSegmentsNamespace < ActiveRecord::Migration[6.0] + def up + execute <<-SQL + UPDATE analytics_devops_adoption_segments SET display_namespace_id = namespace_id + WHERE display_namespace_id IS NULL + SQL + end + + def down + execute 'UPDATE analytics_devops_adoption_segments SET display_namespace_id = NULL' + end +end diff --git a/db/post_migrate/20210511051718_create_index_on_notes_note.rb b/db/post_migrate/20210511051718_create_index_on_notes_note.rb new file mode 100644 index 00000000000..8e0ccb3616b --- /dev/null +++ b/db/post_migrate/20210511051718_create_index_on_notes_note.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateIndexOnNotesNote < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DUPLICATE_INDEX_NAME = 'index_notes_on_note_gin_trigram' + CURRENT_INDEX_NAME = 'index_notes_on_note_trigram' + + disable_ddl_transaction! + + # https://gitlab.com/gitlab-org/gitlab/-/issues/218410#note_565624409 + # We are having troubles with the index, and some inserts are taking a long time + # so in this migration we are recreating the index + def up + add_concurrent_index :notes, :note, name: DUPLICATE_INDEX_NAME, using: :gin, opclass: :gin_trgm_ops + remove_concurrent_index_by_name :notes, CURRENT_INDEX_NAME + end + + def down + add_concurrent_index :notes, :note, name: CURRENT_INDEX_NAME, using: :gin, opclass: :gin_trgm_ops + remove_concurrent_index_by_name :notes, DUPLICATE_INDEX_NAME + end +end diff --git a/db/schema_migrations/20210430122951 b/db/schema_migrations/20210430122951 new file mode 100644 index 00000000000..43c90a10f22 --- /dev/null +++ b/db/schema_migrations/20210430122951 @@ -0,0 +1 @@ +476dc70eae87ad3ee30e6be8c1afb4a2aec23a09b96daba2afbd9c4e2edb12b9 \ No newline at end of file diff --git a/db/schema_migrations/20210430124212 b/db/schema_migrations/20210430124212 new file mode 100644 index 00000000000..dd3e8c1f371 --- /dev/null +++ b/db/schema_migrations/20210430124212 @@ -0,0 +1 @@ +ebdeb56647f3a7ff5620141833c90b796a9ddfed39234bcf8063ca5b3df6c1f3 \ No newline at end of file diff --git a/db/schema_migrations/20210430124630 b/db/schema_migrations/20210430124630 new file mode 100644 index 00000000000..2366ab58ef4 --- /dev/null +++ b/db/schema_migrations/20210430124630 @@ -0,0 +1 @@ +7f6862205e8c315da8433083fc5391f8889951f62d466e0048063322a46f9cc7 \ No newline at end of file diff --git a/db/schema_migrations/20210430130259 b/db/schema_migrations/20210430130259 new file mode 100644 index 00000000000..b8064b30f52 --- /dev/null +++ b/db/schema_migrations/20210430130259 @@ -0,0 +1 @@ +c4a4b214f15a1a8d7f6832782d50077189281ca9a9b1b746a0a3bc3af4a47e3c \ No newline at end of file diff --git a/db/schema_migrations/20210430134202 b/db/schema_migrations/20210430134202 new file mode 100644 index 00000000000..cb9eee98cc0 --- /dev/null +++ b/db/schema_migrations/20210430134202 @@ -0,0 +1 @@ +77e2b8c1c6054a80122f97dda1e843149fefb7bf6694fdfa897d691d61162d81 \ No newline at end of file diff --git a/db/schema_migrations/20210430135954 b/db/schema_migrations/20210430135954 new file mode 100644 index 00000000000..9e201905704 --- /dev/null +++ b/db/schema_migrations/20210430135954 @@ -0,0 +1 @@ +c5fe6f74822168599ad5069bb7c793ec96a4bba99d15ad29cb161ef24291b56d \ No newline at end of file diff --git a/db/schema_migrations/20210511051718 b/db/schema_migrations/20210511051718 new file mode 100644 index 00000000000..cf6cb7c0ee9 --- /dev/null +++ b/db/schema_migrations/20210511051718 @@ -0,0 +1 @@ +2d11da499f49964f37cc0a2c541cf58182416ae7b6b0e762a135b327099de1a4 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6ebfbf4409a..fac2cac7b06 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9103,12 +9103,11 @@ ALTER SEQUENCE analytics_devops_adoption_segment_selections_id_seq OWNED BY anal CREATE TABLE analytics_devops_adoption_segments ( id bigint NOT NULL, - name text, last_recorded_at timestamp with time zone, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, namespace_id integer, - CONSTRAINT check_4be7a006fd CHECK ((char_length(name) <= 255)) + display_namespace_id integer ); CREATE SEQUENCE analytics_devops_adoption_segments_id_seq @@ -9133,7 +9132,8 @@ CREATE TABLE analytics_devops_adoption_snapshots ( security_scan_succeeded boolean NOT NULL, end_time timestamp with time zone NOT NULL, total_projects_count integer, - code_owners_used_count integer + code_owners_used_count integer, + namespace_id integer ); CREATE SEQUENCE analytics_devops_adoption_snapshots_id_seq @@ -22133,6 +22133,12 @@ CREATE INDEX idx_container_repositories_on_exp_cleanup_status_and_start_date ON CREATE INDEX idx_deployment_clusters_on_cluster_id_and_kubernetes_namespace ON deployment_clusters USING btree (cluster_id, kubernetes_namespace); +CREATE INDEX idx_devops_adoption_segments_namespace_end_time ON analytics_devops_adoption_snapshots USING btree (namespace_id, end_time); + +CREATE INDEX idx_devops_adoption_segments_namespace_recorded_at ON analytics_devops_adoption_snapshots USING btree (namespace_id, recorded_at); + +CREATE UNIQUE INDEX idx_devops_adoption_segments_namespaces_pair ON analytics_devops_adoption_segments USING btree (display_namespace_id, namespace_id); + CREATE INDEX idx_eaprpb_external_approval_rule_id ON external_approval_rules_protected_branches USING btree (external_approval_rule_id); CREATE INDEX idx_elastic_reindexing_slices_on_elastic_reindexing_subtask_id ON elastic_reindexing_slices USING btree (elastic_reindexing_subtask_id); @@ -23667,7 +23673,7 @@ CREATE INDEX index_notes_on_discussion_id ON notes USING btree (discussion_id); CREATE INDEX index_notes_on_line_code ON notes USING btree (line_code); -CREATE INDEX index_notes_on_note_trigram ON notes USING gin (note gin_trgm_ops); +CREATE INDEX index_notes_on_note_gin_trigram ON notes USING gin (note gin_trgm_ops); CREATE INDEX index_notes_on_noteable_id_and_noteable_type_and_system ON notes USING btree (noteable_id, noteable_type, system); @@ -25224,6 +25230,9 @@ ALTER TABLE ONLY project_features ALTER TABLE ONLY ci_pipelines ADD CONSTRAINT fk_190998ef09 FOREIGN KEY (external_pull_request_id) REFERENCES external_pull_requests(id) ON DELETE SET NULL; +ALTER TABLE ONLY analytics_devops_adoption_segments + ADD CONSTRAINT fk_190a24754d FOREIGN KEY (display_namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY user_details ADD CONSTRAINT fk_190e4fcc88 FOREIGN KEY (provisioned_by_group_id) REFERENCES namespaces(id) ON DELETE SET NULL; @@ -25440,6 +25449,9 @@ ALTER TABLE ONLY users ALTER TABLE ONLY geo_event_log ADD CONSTRAINT fk_78a6492f68 FOREIGN KEY (repository_updated_event_id) REFERENCES geo_repository_updated_events(id) ON DELETE CASCADE; +ALTER TABLE ONLY analytics_devops_adoption_snapshots + ADD CONSTRAINT fk_78c9eac821 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY lists ADD CONSTRAINT fk_7a5553d60f FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE; diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index cba4deb3f55..f29db9ead38 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -129,6 +129,7 @@ The following metrics are available: | `pipeline_graph_links_per_job_ratio` | Histogram | 13.9 | Ratio of links to job per graph | | | `gitlab_ci_pipeline_security_orchestration_policy_processing_duration_seconds` | Histogram | 13.12 | Time in seconds it takes to process Security Policies in CI/CD pipeline | | | `gitlab_ci_difference_live_vs_actual_minutes` | Histogram | 13.12 | Difference between CI minute consumption counted while jobs were running (live) vs when jobs are complete (actual). Used to enforce CI minute consumption limits on long running jobs. | `plan` | +| `gitlab_spamcheck_request_duration_seconds` | Histogram | 13.12 | The duration for requests between Rails and the anti-spam engine | | ## Metrics controlled by a feature flag diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 404e28c58c6..bf2aba9f88c 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -864,13 +864,6 @@ configuration is tried to be resolved automatically before reporting an error. ### Object storage settings -WARNING: -With the following settings, Pages uses both NFS and Object Storage locations when deploying the -site. **Do not remove the existing NFS mount used by Pages** when applying these settings. For more -information, see the epics -[3901](https://gitlab.com/groups/gitlab-org/-/epics/3901#how-to-test-object-storage-integration-in-beta) -and [3910](https://gitlab.com/groups/gitlab-org/-/epics/3910). - The following settings are: - Nested under `pages:` and then `object_store:` on source installations. @@ -882,6 +875,10 @@ The following settings are: | `remote_directory` | The name of the bucket where Pages site content is stored. | | | `connection` | Various connection options described below. | | +NOTE: +If you want to stop using and disconnect the NFS server, you need to [explicitly disable +local storage](#disable-pages-local-storage), and it's only possible after upgrading to GitLab 13.11. + #### S3-compatible connection settings See [the available connection settings for different providers](../object_storage.md#connection-settings). @@ -915,6 +912,8 @@ In Omnibus installations: 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. +1. [Migrate existing Pages deployments to object storage.](#migrate-pages-deployments-to-object-storage) + In installations from source: 1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines: @@ -934,6 +933,8 @@ In installations from source: 1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect. +1. [Migrate existing Pages deployments to object storage.](#migrate-pages-deployments-to-object-storage) + ## ZIP storage In GitLab 14.0 the underlaying storage format of GitLab Pages is changing from @@ -1019,6 +1020,43 @@ After the migration to object storage is performed, you can choose to revert you sudo gitlab-rake gitlab:pages:deployments:migrate_to_local ``` +### Disable Pages local storage + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/301159) in GitLab 13.11. + +If you use [object storage](#using-object-storage), disable local storage: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['pages_local_store_enabled'] = false + ``` + +1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. + +Starting from GitLab 13.12, this setting also disables the [legacy storage](#migrate-legacy-storage-to-zip-storage), so if you were using NFS to serve Pages, you can completely disconnect from it. + +## Migrate GitLab Pages to 14.0 + +In GitLab 14.0 a number of breaking changes are introduced which may require some user intervention. +The steps below describe the best way to migrate without causing any downtime for your GitLab instance. + +If you run GitLab on a single server, then most likely you will not notice any problem after +upgrading to GitLab 14.0, but it may be safer to follow the steps anyway. +If you run GitLab on a single server, then most likely the upgrade process to 14.0 will go smoothly for you. Regardless, we recommend everyone follow the migration steps to ensure a successful upgrade. +If at any point you run into issues, consult the [troubleshooting section](#troubleshooting). + +To migrate GitLab Pages to GitLab 14.0: + +1. If your current GitLab version is lower than 13.12, then you first need to upgrade to 13.12. +Upgrading directly to 14.0 may cause downtime for some web-sites hosted on GitLab Pages +until you finish the following steps. +1. Enable the [API-based configuration](#gitlab-api-based-configuration), which +is the default starting from GitLab 14.0. Skip this step if you're already running GitLab 14.0 or above. +1. If you want to store your pages content in the [object storage](#using-object-storage), make sure to configure it. +If you want to store the pages content locally or continue using an NFS server, skip this step. +1. [Migrate legacy storage to ZIP storage.](#migrate-legacy-storage-to-zip-storage) + ## Backup GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md), so there is no separate backup to configure. diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md index 8198130c61e..e570b9f31cf 100644 --- a/doc/api/epic_links.md +++ b/doc/api/epic_links.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9188) in GitLab 11.8. -Manages parent-child [epic relationships](../user/group/epics/index.md#multi-level-child-epics). +Manages parent-child [epic relationships](../user/group/epics/manage_epics.md#multi-level-child-epics). Every API call to `epic_links` must be authenticated. diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 51724d32f3a..c3125f52cf2 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -895,6 +895,27 @@ When you want to ensure that no event got called, you can use `expect_no_snowplo end ``` +#### Test Snowplow context against the schema + +The [Snowplow schema matcher](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60480) +helps to reduce validation errors by testing Snowplow context against the JSON schema. +The schema matcher accepts the following parameters: + +- `schema path` +- `context` + +To add a schema matcher spec: + +1. Add a new schema to the [Iglu repository](https://gitlab.com/gitlab-org/iglu), + then copy the same schema to the `spec/fixtures/product_intelligence/` directory. +1. In the copied schema, remove the `"$schema"` key and value. We do not need it for specs + and the spec fails if we keep the key, as it tries to look for the schema in the URL. +1. Use the following snippet to call the schema matcher: + + ```ruby + match_snowplow_context_schema(schema_path: '', context: ) + ``` + ### Table-based / Parameterized tests This style of testing is used to exercise one piece of code with a comprehensive diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 806a3435a9f..1ce2695901a 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -2134,6 +2134,294 @@ Status: `data_available` Tiers: `free`, `premium`, `ultimate` +### `counts.in_product_marketing_email_create_0_cta_clicked` + +Total clicks on the create track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510201919_in_product_marketing_email_create_0_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_create_0_sent` + +Total sent emails of the create track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510201537_in_product_marketing_email_create_0_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_create_1_cta_clicked` + +Total clicks on the create track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202356_in_product_marketing_email_create_1_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_create_1_sent` + +Total sent emails of the create track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202148_in_product_marketing_email_create_1_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_create_2_cta_clicked` + +Total clicks on the create track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202724_in_product_marketing_email_create_2_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_create_2_sent` + +Total sent emails of the create track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202604_in_product_marketing_email_create_2_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_team_0_cta_clicked` + +Total clicks on the team track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203143_in_product_marketing_email_team_0_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_team_0_sent` + +Total sent emails of the team track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203134_in_product_marketing_email_team_0_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_team_1_cta_clicked` + +Total clicks on the team track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203203_in_product_marketing_email_team_1_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_team_1_sent` + +Total sent emails of the team track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203153_in_product_marketing_email_team_1_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_team_2_cta_clicked` + +Total clicks on the team track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203223_in_product_marketing_email_team_2_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_team_2_sent` + +Total sent emails of the team track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203213_in_product_marketing_email_team_2_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_trial_0_cta_clicked` + +Total clicks on the verify trial's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203044_in_product_marketing_email_trial_0_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_trial_0_sent` + +Total sent emails of the trial track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203035_in_product_marketing_email_trial_0_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_trial_1_cta_clicked` + +Total clicks on the trial track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203104_in_product_marketing_email_trial_1_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_trial_1_sent` + +Total sent emails of the trial track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203054_in_product_marketing_email_trial_1_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_trial_2_cta_clicked` + +Total clicks on the trial track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203124_in_product_marketing_email_trial_2_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_trial_2_sent` + +Total sent emails of the trial track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203114_in_product_marketing_email_trial_2_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_verify_0_cta_clicked` + +Total clicks on the verify track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202943_in_product_marketing_email_verify_0_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_verify_0_sent` + +Total sent emails of the verify track's first email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202807_in_product_marketing_email_verify_0_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_verify_1_cta_clicked` + +Total clicks on the verify track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203005_in_product_marketing_email_verify_1_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_verify_1_sent` + +Total sent emails of the verify track's second email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510202955_in_product_marketing_email_verify_1_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_verify_2_cta_clicked` + +Total clicks on the verify track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203025_in_product_marketing_email_verify_2_cta_clicked.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + +### `counts.in_product_marketing_email_verify_2_sent` + +Total sent emails of the verify track's third email + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210510203015_in_product_marketing_email_verify_2_sent.yml) + +Group: `group::activation` + +Status: `implemented` + +Tiers: `free`, `premium`, `ultimate` + ### `counts.in_review_folder` Missing description diff --git a/doc/user/group/epics/img/epic_view_v13.0.png b/doc/user/group/epics/img/epic_view_v13.0.png deleted file mode 100644 index c317a5707bce04931c88355af4a2a6f3fda228a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51112 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 diff --git a/doc/user/group/epics/img/new_epic_from_groups_v13.7.png b/doc/user/group/epics/img/new_epic_from_groups_v13.7.png deleted file mode 100644 index 3607d5c7a3fe580d88afec007b4ff122a1909303..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10505 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 - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2. -> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8. +> - Single-level epics [were moved](https://gitlab.com/gitlab-org/gitlab/-/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8. -Epics let you manage your portfolio of projects more efficiently by tracking groups of [issues](../../project/issues/index.md) -that share a theme across projects and milestones. +When [issues](../../project/issues/index.md) share a theme across projects and milestones, +you can manage them by using epics. -An epic's page contains the following tabs: +You can also create child epics, and assign start and end dates, +which creates a visual roadmap for you to view progress. -- **Issues**: issues added to this epic. -- **Epics and Issues**: epics and issues added to this epic. - Appears instead of the **Issues** tab if you have access to [multi-level epics](#multi-level-child-epics). - Child epics and their issues are shown in a tree view. +Use epics: - - To reveal the child epics and issues, select the chevron (**>**) next to a parent epic. - - To see a breakdown of open and closed items, hover over the total counts. - - The number provided here includes all epics associated with this project. The number includes - epics for which users may not yet have permission. - -- [**Roadmap**](#roadmap-in-epics): a roadmap view of child epics which have start and due dates. - Appears if you have access to [multi-level epics](#multi-level-child-epics). - -![epic view](img/epic_view_v13.0.png) - -## Use cases - -- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature. -- Track when the work for the group of issues is targeted to begin, and when it's targeted to end. -- Discuss and collaborate on feature ideas and scope at a high level. - -## Manage epics - -To learn what you can do with an epic, see [Manage epics](manage_epics.md). Possible actions include: - -- [Create an epic](manage_epics.md#create-an-epic) -- [Edit an epic](manage_epics.md#edit-an-epic) -- [Bulk-edit epics](manage_epics.md#bulk-edit-epics) -- [Delete an epic](manage_epics.md#delete-an-epic) -- [Close an epic](manage_epics.md#close-an-epic) -- [Reopen a closed epic](manage_epics.md#reopen-a-closed-epic) -- [Go to an epic from an issue](manage_epics.md#go-to-an-epic-from-an-issue) -- [Search for an epic from epics list page](manage_epics.md#search-for-an-epic-from-epics-list-page) -- [Make an epic confidential](manage_epics.md#make-an-epic-confidential) -- [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic) -- [Manage multi-level child epics **(ULTIMATE)**](manage_epics.md#manage-multi-level-child-epics) +- When your team is working on a large feature that involves multiple discussions + in different issues in different projects in a [group](../index.md). +- To track when the work for the group of issues is targeted to begin and end. +- To discuss and collaborate on feature ideas and scope at a high level. ## Relationships between epics and issues The possible relationships between epics and issues are: - An epic is the parent of one or more issues. -- An epic is the parent of one or more child epics. For details see [Multi-level child epics](#multi-level-child-epics). **(ULTIMATE)** +- An epic is the parent of one or more child epics. For details see [Multi-level child epics](manage_epics.md#multi-level-child-epics). ```mermaid graph TD @@ -67,72 +37,13 @@ graph TD Child_epic --> Issue2 ``` -See [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic) for steps -to: - -- Add an issue to an epic -- Reorder issues -- Move an issue between epics -- Promote an issue to an epic - -## Issue health status in Epic tree **(ULTIMATE)** - -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/199184) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. -> - The health status of a closed issue [is hidden](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3 or later. -> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/213567) in GitLab 13.7. - -Report or respond to the health of issues and epics by setting a red, amber, or green [health status](../../project/issues/managing_issues.md#health-status), which then appears on your Epic tree. - -## Multi-level child epics **(ULTIMATE)** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8333) in GitLab Ultimate 11.7. - -You can add any epic that belongs to a group or subgroup of the parent epic's group. -New child epics appear at the top of the list of epics in the **Epics and Issues** tab. - -When you add an epic that's already linked to a parent epic, the link to its current parent is removed. - -Epics can contain multiple nested child epics, up to a total of seven levels deep. - -See [Manage multi-level child epics](manage_epics.md#manage-multi-level-child-epics) for -steps to create, move, reorder, or delete child epics. - -## Start date and due date - -To set a **Start date** and **Due date** for an epic, select one of the following: - -- **Fixed**: enter a fixed value. -- **Inherited**: inherit a dynamic value from the epic's issues, child epics, and milestones. - -### Inherited - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7332) in GitLab 12.5 to replace **From milestones**. - -If you select **Inherited**: - -- For the **start date**: GitLab scans all child epics and issues assigned to the epic, - and sets the start date to match the earliest found start date or milestone. -- For the **due date**: GitLab sets the due date to match the latest due date or - milestone found among its child epics and issues. - -These are dynamic dates and recalculated if any of the following occur: - -- A child epic's dates change. -- Milestones are reassigned to an issue. -- A milestone's dates change. -- Issues are added to, or removed from, the epic. - -Because the epic's dates can inherit dates from its children, the start date and due date propagate from the bottom to the top. -If the start date of a child epic on the lowest level changes, that becomes the earliest possible start date for its parent epic. -The parent epic's start date then reflects this change and propagates upwards to the top epic. - ## Roadmap in epics **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10. -If your epic contains one or more [child epics](#multi-level-child-epics) which -have a [start or due date](#start-date-and-due-date), a -[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic. +If your epic contains one or more [child epics](manage_epics.md#multi-level-child-epics) that +have a start or due date, a visual +[roadmap](../roadmap/index.md) of the child epics is listed under the parent epic. ![Child epics roadmap](img/epic_view_roadmap_v12_9.png) @@ -144,45 +55,19 @@ epic's issue list. If you have access to edit an epic and an issue added to that epic, you can add the issue to or remove it from the epic. -Note that for a given group, the visibility of all projects must be the same as +For a given group, the visibility of all projects must be the same as the group, or less restrictive. That means if you have access to a group's epic, then you already have access to its projects' issues. You can also consult the [group permissions table](../../permissions.md#group-members-permissions). -## Thread +## Related topics -- Comments: collaborate on that epic by posting comments in its thread. - These text fields also fully support - [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown). - -## Comment or start a thread - -Once you write your comment, you can either: - -- Click **Comment** to publish your comment. -- Click **Start thread** to start a thread within that epic's discussion. - -### Activity sort order - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214364) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. - -You can reverse the default order and interact with the activity feed sorted by most recent items -at the top. Your preference is saved via local storage and automatically applied to every issue -you view. - -To change the activity sort order, click the **Oldest first** dropdown menu and select either oldest -or newest items to be shown first. - -![Issue activity sort order dropdown button](img/epic_activity_sort_order_v13_2.png) - -## Award emoji - -You can [award an emoji](../../award_emojis.md) to that epic or its comments. - -## Notifications - -You can [turn on notifications](../../profile/notifications.md) to be alerted about epic events. +- [Manage epics](manage_epics.md) and multi-level child epics. +- [Turn on notifications](../../profile/notifications.md) for about epic events. +- [Award an emoji](../../award_emojis.md) to an epic or its comments. +- Collaborate on an epic by posting comments in a [thread](../../discussions/index.md). +- Use [health status](../../project/issues/managing_issues.md#health-status) to track your progress.