summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-11 12:12:30 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-11 12:12:30 +0000
commit0b54f87a31c23544ca5917bf772ce9c64a61562c (patch)
tree79d56df6750e84fd4a10205d9dcce293f7c5d491
parente348fb4c1b9eaf21655001dc4346ceb0c0c3d5b4 (diff)
downloadgitlab-ce-0b54f87a31c23544ca5917bf772ce9c64a61562c.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/qa-common/main.gitlab-ci.yml13
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/test-on-gdk/main.gitlab-ci.yml6
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml10
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--.rubocop_todo/rspec/verified_doubles.yml1
-rw-r--r--app/assets/javascripts/notes/constants.js45
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js39
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue66
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue27
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue2
-rw-r--r--app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql9
-rw-r--r--app/controllers/projects/grafana_api_controller.rb2
-rw-r--r--app/controllers/projects/jobs_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/projects/metrics/dashboards/builder_controller.rb2
-rw-r--r--app/controllers/projects/performance_monitoring/dashboards_controller.rb4
-rw-r--r--app/finders/pending_todos_finder.rb28
-rw-r--r--app/helpers/ci/jobs_helper.rb18
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/packages/package.rb7
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb121
-rw-r--r--app/models/user_preference.rb4
-rw-r--r--app/models/users/group_callout.rb6
-rw-r--r--app/models/users/phone_number_validation.rb33
-rw-r--r--app/models/users/project_callout.rb6
-rw-r--r--app/models/users/user_follow_user.rb10
-rw-r--r--app/models/web_ide_terminal.rb14
-rw-r--r--app/models/webauthn_registration.rb2
-rw-r--r--app/models/wiki_page.rb10
-rw-r--r--app/models/work_item.rb6
-rw-r--r--app/services/keys/last_used_service.rb24
-rw-r--r--app/services/members/approve_access_request_service.rb2
-rw-r--r--app/services/members/base_service.rb4
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/projects/open_issues_count_service.rb2
-rw-r--r--app/services/todo_service.rb32
-rw-r--r--app/views/projects/snippets/index.html.haml2
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/ssh_keys/update_last_used_at_worker.rb21
-rw-r--r--config/feature_flags/development/anthropic_experimentation.yml8
-rw-r--r--config/feature_flags/development/application_settings_tokens_optional_encryption.yml8
-rw-r--r--config/feature_flags/development/expose_authorized_cluster_agents.yml2
-rw-r--r--config/feature_flags/development/groups_tokens_optional_encryption.yml2
-rw-r--r--config/feature_flags/development/packages_display_last_pipeline.yml8
-rw-r--r--config/feature_flags/development/projects_tokens_optional_encryption.yml2
-rw-r--r--config/feature_flags/development/realtime_approvals.yml8
-rw-r--r--config/metrics/counts_7d/20230509085219_i_quickactions_blocked_by_weekly.yml23
-rw-r--r--config/metrics/counts_7d/20230509090906_i_quickactions_blocks_weekly.yml23
-rw-r--r--config/routes.rb1
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--data/removals/16_0/16-0-ci-builds-column-validations.yml24
-rw-r--r--data/removals/16_0/16-0-grafana-chart.yml19
-rw-r--r--data/removals/16_0/16-0-limit-ci-job-token.yml32
-rw-r--r--data/removals/16_0/16-0-pipeline_activity_limit.yml12
-rw-r--r--data/removals/16_0/16-0-redis-config-env.yml13
-rw-r--r--data/removals/16_0/16-0-redis-localhost.yml16
-rw-r--r--data/removals/16_0/16-0-terraform-latest-stable-change.yml21
-rw-r--r--doc/api/graphql/removed_items.md2
-rw-r--r--doc/api/rest/deprecations.md8
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md10
-rw-r--r--doc/development/ai_features.md78
-rw-r--r--doc/development/testing_guide/flaky_tests.md2
-rw-r--r--doc/update/removals.md102
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md1
-rw-r--r--doc/user/project/git_attributes.md27
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--doc/user/workspace/index.md164
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/banzai/filter/inline_embeds_filter.rb2
-rw-r--r--lib/banzai/filter/inline_observability_filter.rb16
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb6
-rw-r--r--lib/gitlab/ci/templates/Terraform.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml5
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb2
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/quick_actions/relate_actions.rb34
-rw-r--r--lib/gitlab/usage_data_counters/known_events/quickactions.yml4
-rw-r--r--lib/sidebars/groups/menus/issues_menu.rb2
-rw-r--r--lib/sidebars/groups/super_sidebar_menus/manage_menu.rb4
-rw-r--r--lib/sidebars/groups/super_sidebar_menus/plan_menu.rb2
-rw-r--r--lib/sidebars/projects/menus/issues_menu.rb4
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/manage_menu.rb4
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb3
-rw-r--r--lib/sidebars/projects/super_sidebar_menus/plan_menu.rb3
-rw-r--r--locale/gitlab.pot38
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock10
-rw-r--r--scripts/allowed_warnings.txt5
-rw-r--r--scripts/rspec_helpers.sh12
-rwxr-xr-xscripts/verify-tff-mapping6
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb9
-rw-r--r--spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb30
-rw-r--r--spec/features/issues/form_spec.rb2
-rw-r--r--spec/features/markdown/metrics_spec.rb24
-rw-r--r--spec/features/projects/blobs/blame_spec.rb2
-rw-r--r--spec/features/projects/ci/editor_spec.rb4
-rw-r--r--spec/finders/pending_todos_finder_spec.rb57
-rw-r--r--spec/frontend/blame/blame_redirect_spec.js1
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js7
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js58
-rw-r--r--spec/frontend/ide/components/ide_spec.js7
-rw-r--r--spec/frontend/notes/components/mr_discussion_filter_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js82
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js133
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js16
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js6
-rw-r--r--spec/frontend/work_items/mock_data.js18
-rw-r--r--spec/helpers/ci/jobs_helper_spec.rb53
-rw-r--r--spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb6
-rw-r--r--spec/lib/sidebars/groups/super_sidebar_menus/manage_menu_spec.rb4
-rw-r--r--spec/lib/sidebars/groups/super_sidebar_menus/plan_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/manage_menu_spec.rb4
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb3
-rw-r--r--spec/models/application_setting_spec.rb8
-rw-r--r--spec/models/group_spec.rb5
-rw-r--r--spec/models/key_spec.rb2
-rw-r--r--spec/models/packages/package_spec.rb14
-rw-r--r--spec/requests/projects/metrics/dashboards/builder_spec.rb16
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_test1_and_test2_have_when.yml46
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_with_allow_failure_test1_and_test2_have_when.yml47
-rw-r--r--spec/services/keys/last_used_service_spec.rb58
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb18
-rw-r--r--spec/services/members/base_service_spec.rb5
-rw-r--r--spec/services/members/destroy_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb123
-rw-r--r--spec/services/todo_service_spec.rb99
-rw-r--r--spec/support/capybara_wait_for_all_requests.rb10
-rw-r--r--spec/support/fast_quarantine.rb50
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb123
-rw-r--r--spec/support_specs/capybara_wait_for_all_requests_spec.rb20
-rw-r--r--spec/tooling/lib/tooling/fast_quarantine_spec.rb193
-rw-r--r--spec/tooling/rspec_flaky/config_spec.rb19
-rw-r--r--spec/workers/ssh_keys/update_last_used_at_worker_spec.rb23
-rw-r--r--tests.yml6
-rw-r--r--tooling/lib/tooling/fast_quarantine.rb57
-rw-r--r--tooling/rspec_flaky/config.rb4
157 files changed, 2019 insertions, 965 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c4469ea98a3..9b9b217126b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -165,7 +165,7 @@ variables:
RSPEC_PACKED_TESTS_MAPPING_PATH: crystalball/packed-mapping.json
RSPEC_PROFILING_FOLDER_PATH: rspec/profiling
RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json
- RSPEC_FAST_QUARANTINE_PATH: rspec/fast_quarantine-gitlab.txt
+ RSPEC_FAST_QUARANTINE_LOCAL_PATH: rspec/fast_quarantine-gitlab.txt
TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests"
TMP_TEST_GITLAB_WORKHORSE_PATH: "${TMP_TEST_FOLDER}/${GITLAB_WORKHORSE_FOLDER}"
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
index 3c83d7ace22..3be1bc955c5 100644
--- a/.gitlab/ci/qa-common/main.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -205,11 +205,6 @@ stages:
- .qa-install
- .ruby-image
stage: report
- variables:
- QA_FAILURES_REPORTING_PROJECT: gitlab-org/gitlab
- QA_FAILURES_MAX_DIFF_RATIO: "0.15"
- QA_RSPEC_JSON_FILE_PATTERN: $CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json
- GITLAB_QA_ACCESS_TOKEN: $QA_GITLAB_CI_TOKEN
when: always
script:
- |
@@ -218,10 +213,10 @@ stages:
exit 0
fi
- |
- bundle exec gitlab-qa-report \
- --relate-failure-issue "$QA_RSPEC_JSON_FILE_PATTERN" \
- --project "$QA_FAILURES_REPORTING_PROJECT" \
- --max-diff-ratio "$QA_FAILURES_MAX_DIFF_RATIO"
+ bundle exec relate-failure-issue \
+ --input-files "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json" \
+ --project "gitlab-org/gitlab" \
+ --token "$RELATE_TEST_FAILURE_TOKEN"
.generate-test-session:
extends:
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 17fd8ae38f9..a4041f771d9 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -346,7 +346,7 @@ rspec:flaky-tests-report:
# so we use `dependencies` here.
dependencies: !reference ["rspec:coverage", "dependencies"]
variables:
- SKIPPED_FLAKY_TESTS_REPORT_PATH: rspec/flaky/skipped_flaky_tests_report.txt
+ SKIPPED_TESTS_REPORT_PATH: rspec/skipped_tests_report.txt
RETRIED_TESTS_REPORT_PATH: rspec/flaky/retried_tests_report.txt
before_script:
- source scripts/utils.sh
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
index 5cf38d578e5..c17618ef724 100644
--- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
@@ -34,6 +34,7 @@ include:
expire_in: 7 days
reports:
junit: test_output/**/rspec-*.xml
+ dotenv: suite_status.env
script:
- echo -e "\e[0Ksection_start:`date +%s`:pull_image\r\e[0KPull GDK QA image"
- docker pull ${QA_GDK_IMAGE}
@@ -54,6 +55,11 @@ include:
${QA_GDK_IMAGE} "${CI_COMMIT_SHA}" "$RSPEC_REPORT_OPTS $TEST_GDK_TAGS --tag ~requires_praefect"
# The above image's launch script takes two arguments only - first one is the commit sha and the second one Rspec Args
allow_failure: true
+ after_script:
+ - |
+ if [ "$CI_JOB_STATUS" == "failed" ]; then
+ echo "SUITE_FAILED=true" >> suite_status.env
+ fi
download-knapsack-report:
extends:
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 944771fb3cc..c9911bfe433 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -540,16 +540,6 @@ Layout/ArgumentAlignment:
- 'app/models/terraform/state.rb'
- 'app/models/time_tracking/timelog_category.rb'
- 'app/models/u2f_registration.rb'
- - 'app/models/user.rb'
- - 'app/models/user_preference.rb'
- - 'app/models/users/group_callout.rb'
- - 'app/models/users/phone_number_validation.rb'
- - 'app/models/users/project_callout.rb'
- - 'app/models/users/user_follow_user.rb'
- - 'app/models/web_ide_terminal.rb'
- - 'app/models/webauthn_registration.rb'
- - 'app/models/wiki_page.rb'
- - 'app/models/work_item.rb'
- 'app/services/compare_service.rb'
- 'app/services/concerns/rate_limited_service.rb'
- 'app/services/design_management/copy_design_collection/copy_service.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index af8f9dd3cb1..765a9a81d21 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -4849,7 +4849,6 @@ Layout/LineLength:
- 'spec/services/lfs/push_service_spec.rb'
- 'spec/services/loose_foreign_keys/batch_cleaner_service_spec.rb'
- 'spec/services/loose_foreign_keys/cleaner_service_spec.rb'
- - 'spec/services/members/approve_access_request_service_spec.rb'
- 'spec/services/members/create_service_spec.rb'
- 'spec/services/members/destroy_service_spec.rb'
- 'spec/services/members/invitation_reminder_email_service_spec.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index 40f4516abb4..943fded777c 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -510,7 +510,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/helpers/ee/subscribable_banner_helper_spec.rb'
- 'ee/spec/helpers/ee/system_note_helper_spec.rb'
- 'ee/spec/helpers/ee/todos_helper_spec.rb'
- - 'ee/spec/helpers/ee/trial_registration_helper_spec.rb'
- 'ee/spec/helpers/ee/users/callouts_helper_spec.rb'
- 'ee/spec/helpers/ee/version_check_helper_spec.rb'
- 'ee/spec/helpers/ee/welcome_helper_spec.rb'
diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml
index 217b59e0ef0..88b6f302e56 100644
--- a/.rubocop_todo/rspec/verified_doubles.yml
+++ b/.rubocop_todo/rspec/verified_doubles.yml
@@ -31,7 +31,6 @@ RSpec/VerifiedDoubles:
- 'ee/spec/helpers/ee/ci/runners_helper_spec.rb'
- 'ee/spec/helpers/ee/integrations_helper_spec.rb'
- 'ee/spec/helpers/ee/subscribable_banner_helper_spec.rb'
- - 'ee/spec/helpers/ee/trial_registration_helper_spec.rb'
- 'ee/spec/helpers/kerberos_helper_spec.rb'
- 'ee/spec/helpers/license_helper_spec.rb'
- 'ee/spec/helpers/roadmaps_helper_spec.rb'
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index e7c3385ae5c..419b427682e 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -61,17 +61,7 @@ export const MR_FILTER_OPTIONS = [
{
text: __('Approvals'),
value: 'approval',
- systemNoteIcons: ['approval', 'unapproval'],
- },
- {
- text: __('Commits & branches'),
- value: 'commit_branches',
- systemNoteIcons: ['commit', 'fork'],
- },
- {
- text: __('Merge request status'),
- value: 'status',
- systemNoteIcons: ['git-merge', 'issue-close', 'issues'],
+ systemNoteIcons: ['approval', 'unapproval', 'check'],
},
{
text: __('Assignees & reviewers'),
@@ -84,6 +74,18 @@ export const MR_FILTER_OPTIONS = [
],
},
{
+ text: __('Comments'),
+ value: 'comments',
+ noteType: ['DiscussionNote', 'DiffNote'],
+ individualNote: true,
+ noteText: [s__('IssuableEvents|resolved all threads')],
+ },
+ {
+ text: __('Commits & branches'),
+ value: 'commit_branches',
+ systemNoteIcons: ['commit', 'fork'],
+ },
+ {
text: __('Edits'),
value: 'edits',
systemNoteIcons: ['pencil', 'task-done'],
@@ -94,25 +96,24 @@ export const MR_FILTER_OPTIONS = [
systemNoteIcons: ['label'],
},
{
+ text: __('Lock status'),
+ value: 'lock_status',
+ systemNoteIcons: ['lock', 'lock-open'],
+ },
+ {
text: __('Mentions'),
value: 'mentions',
systemNoteIcons: ['comment-dots'],
},
{
+ text: __('Merge request status'),
+ value: 'status',
+ systemNoteIcons: ['git-merge', 'issue-close', 'issues', 'merge-request-close'],
+ },
+ {
text: __('Tracking'),
value: 'tracking',
noteType: ['MilestoneNote'],
systemNoteIcons: ['timer'],
},
- {
- text: __('Comments'),
- value: 'comments',
- noteType: ['DiscussionNote', 'DiffNote'],
- individualNote: true,
- },
- {
- text: __('Lock status'),
- value: 'lock_status',
- systemNoteIcons: ['lock', 'lock-open'],
- },
];
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
index dd22d29d9a7..c13d49b5379 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
@@ -225,9 +225,6 @@ export default {
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
- <template #secondStrong="{ content }">
- <strong>{{ content }}</strong>
- </template>
</gl-sprintf>
</p>
<expiration-dropdown
@@ -264,9 +261,6 @@ export default {
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
- <template #secondStrong="{ content }">
- <strong>{{ content }}</strong>
- </template>
</gl-sprintf>
</p>
<expiration-dropdown
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 5f59372e5ba..05616a0a4f6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -29,7 +29,7 @@ export const TEXT_AREA_INVALID_FEEDBACK = s__(
export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags');
export const KEEP_INFO_TEXT = s__(
- 'ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept.',
+ 'ContainerRegistry|Tags that match %{strongStart}any of%{strongEnd} these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{strongStart}latest%{strongEnd} tag is always kept.',
);
export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:');
export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql
new file mode 100644
index 00000000000..d5092d9ae1a
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql
@@ -0,0 +1,17 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+subscription mergeRequestApprovalStateUpdated($issuableId: IssuableID!) {
+ mergeRequestApprovalStateUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ id
+ approvedBy {
+ nodes {
+ ...User
+ }
+ }
+ userPermissions {
+ canApprove
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
index 7e658e77d37..3228c09c9b6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
@@ -1,5 +1,11 @@
-import { createAlert } from '~/alert';
+import mergeRequestApprovalStateUpdated from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
+
+import { createAlert } from '~/alert';
+
+import { convertToGraphQLId } from '../../graphql_shared/utils';
+import { TYPENAME_MERGE_REQUEST } from '../../graphql_shared/constants';
+
import { FETCH_ERROR } from '../components/approvals/messages';
export default {
@@ -25,6 +31,29 @@ export default {
message: FETCH_ERROR,
});
},
+ subscribeToMore: {
+ document: mergeRequestApprovalStateUpdated,
+ variables() {
+ return {
+ issuableId: convertToGraphQLId(TYPENAME_MERGE_REQUEST, this.mr.id),
+ };
+ },
+ skip() {
+ return !this.mr?.id || !this.isRealtimeEnabled;
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestApprovalStateUpdated: queryResult },
+ },
+ },
+ ) {
+ if (queryResult) {
+ this.mr.setApprovals(queryResult);
+ }
+ },
+ },
},
},
data() {
@@ -34,6 +63,14 @@ export default {
disableCommittersApproval: false,
};
},
+ computed: {
+ isRealtimeEnabled() {
+ // This mixin needs glFeatureFlagsMixin, but fatals if it's included here.
+ // Parents that include this mixin (approvals) should also include the
+ // glFeatureFlagsMixin mixin, or this will always be false.
+ return Boolean(this.glFeatures?.realtimeApprovals);
+ },
+ },
methods: {
clearError() {
this.$emit('clearError');
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 56802f76bba..e3ac49b90d5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -438,15 +438,15 @@ export default {
this.activeToast?.hide();
}
},
- async removeChild(childId) {
+ async removeChild({ id }) {
try {
- const { data } = await this.updateWorkItem(null, childId, null);
+ const { data } = await this.updateWorkItem(null, id, null);
if (data.workItemUpdate.errors.length === 0) {
this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
action: {
text: s__('WorkItem|Undo'),
- onClick: this.undoChildRemoval.bind(this, data.workItemUpdate.workItem, childId),
+ onClick: this.undoChildRemoval.bind(this, data.workItemUpdate.workItem, id),
},
});
}
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 376263503de..f8422dda211 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -2,7 +2,6 @@
import { GlAlert, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
-import deleteWorkItemFromTaskMutation from '../graphql/delete_task_from_work_item.mutation.graphql';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
export default {
@@ -27,26 +26,6 @@ export default {
required: false,
default: null,
},
- issueGid: {
- type: String,
- required: false,
- default: '',
- },
- lockVersion: {
- type: Number,
- required: false,
- default: null,
- },
- lineNumberStart: {
- type: String,
- required: false,
- default: null,
- },
- lineNumberEnd: {
- type: String,
- required: false,
- default: null,
- },
},
emits: ['workItemDeleted', 'close', 'update-modal'],
data() {
@@ -75,50 +54,6 @@ export default {
},
methods: {
deleteWorkItem() {
- if (this.lockVersion != null && this.lineNumberStart && this.lineNumberEnd) {
- this.deleteWorkItemWithTaskData();
- } else {
- this.deleteWorkItemWithoutTaskData();
- }
- },
- deleteWorkItemWithTaskData() {
- this.$apollo
- .mutate({
- mutation: deleteWorkItemFromTaskMutation,
- variables: {
- input: {
- id: this.issueGid,
- lockVersion: this.lockVersion,
- taskData: {
- id: this.workItemId,
- lineNumberStart: Number(this.lineNumberStart),
- lineNumberEnd: Number(this.lineNumberEnd),
- },
- },
- },
- })
- .then(
- ({
- data: {
- workItemDeleteTask: {
- workItem: { descriptionHtml },
- errors,
- },
- },
- }) => {
- if (errors?.length) {
- throw new Error(errors[0]);
- }
-
- this.$emit('workItemDeleted', descriptionHtml);
- this.hide();
- },
- )
- .catch((error) => {
- this.setErrorMessage(error.message);
- });
- },
- deleteWorkItemWithoutTaskData() {
this.$apollo
.mutate({
mutation: deleteWorkItemMutation,
@@ -191,7 +126,6 @@ export default {
<work-item-detail
is-modal
- :work-item-parent-id="issueGid"
:work-item-id="displayedWorkItemId"
:work-item-iid="displayedWorkItemIid"
class="gl-p-5 gl-mt-n3 gl-reset-bg gl-isolation-isolate"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 8152412a5c1..401c8a53eb0 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -184,19 +184,19 @@ export default {
showScopedLabel(label) {
return isScopedLabel(label) && this.allowsScopedLabels;
},
- async removeChild(childId) {
+ async removeChild({ id }) {
this.cloneChildren();
this.isLoadingChildren = true;
try {
- const { data } = await this.updateWorkItem(childId, null);
+ const { data } = await this.updateWorkItem(id, null);
if (!data?.workItemUpdate?.errors?.length) {
- this.filterRemovedChild(childId);
+ this.filterRemovedChild(id);
this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
action: {
text: s__('WorkItem|Undo'),
- onClick: this.undoChildRemoval.bind(this, childId),
+ onClick: this.undoChildRemoval.bind(this, id),
},
});
}
@@ -341,7 +341,7 @@ export default {
:work-item-id="childItem.id"
:parent-work-item-id="issuableGid"
data-testid="links-menu"
- @removeChild="$emit('removeChild', childItem.id)"
+ @removeChild="$emit('removeChild', childItem)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 46c109f2d57..5728e33880e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -185,29 +185,26 @@ export default {
variables: { id: this.issuableGid, workItem },
});
},
- async updateWorkItem(workItem, childId, parentId) {
- const response = await this.$apollo.mutate({
+ async undoChildRemoval(workItem, childId) {
+ const { data } = await this.$apollo.mutate({
mutation: updateWorkItemMutation,
- variables: { input: { id: childId, hierarchyWidget: { parentId } } },
+ variables: { input: { id: childId, hierarchyWidget: { parentId: this.issuableGid } } },
});
- if (parentId === null) {
- await this.removeHierarchyChild(workItem);
- } else {
- await this.addHierarchyChild(workItem);
- }
-
- return response;
- },
- async undoChildRemoval(workItem, childId) {
- const { data } = await this.updateWorkItem(workItem, childId, this.issuableGid);
+ await this.addHierarchyChild(workItem);
if (data.workItemUpdate.errors.length === 0) {
this.activeToast?.hide();
}
},
- async removeChild(childId) {
- const { data } = await this.updateWorkItem({ id: childId }, childId, null);
+ async removeChild(workItem) {
+ const childId = workItem.id;
+ const { data } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: { input: { id: childId, hierarchyWidget: { parentId: null } } },
+ });
+
+ await this.removeHierarchyChild(workItem);
if (data.workItemUpdate.errors.length === 0) {
this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 121c987da71..2cabf489bc6 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -36,7 +36,7 @@ export default {
:issuable-gid="workItemId"
:child-item="child"
:work-item-type="workItemType"
- @removeChild="$emit('removeChild', child.id)"
+ @removeChild="$emit('removeChild', $event)"
@click="$emit('click', Object.assign($event, { childItem: child }))"
/>
</div>
diff --git a/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql
deleted file mode 100644
index 32c07ed48c7..00000000000
--- a/app/assets/javascripts/work_items/graphql/delete_task_from_work_item.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation workItemDeleteTask($input: WorkItemDeleteTaskInput!) {
- workItemDeleteTask(input: $input) {
- workItem {
- id
- descriptionHtml
- }
- errors
- }
-}
diff --git a/app/controllers/projects/grafana_api_controller.rb b/app/controllers/projects/grafana_api_controller.rb
index 9cd511f6a11..2cc6c6c35ba 100644
--- a/app/controllers/projects/grafana_api_controller.rb
+++ b/app/controllers/projects/grafana_api_controller.rb
@@ -10,6 +10,8 @@ class Projects::GrafanaApiController < Projects::ApplicationController
urgency :low
def proxy
+ return not_found if Feature.enabled?(:remove_monitor_metrics)
+
result = ::Grafana::ProxyService.new(
project,
params[:datasource_id],
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 36fa1fab68f..79ddcbf732d 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -8,8 +8,8 @@ class Projects::JobsController < Projects::ApplicationController
urgency :low, [:index, :show, :trace, :retry, :play, :cancel, :unschedule, :erase, :raw]
- before_action :find_job_as_build, except: [:index, :play, :show, :retry]
- before_action :find_job_as_processable, only: [:play, :show, :retry]
+ before_action :find_job_as_build, except: [:index, :play, :retry]
+ before_action :find_job_as_processable, only: [:play, :retry]
before_action :authorize_read_build_trace!, only: [:trace, :raw]
before_action :authorize_read_build!
before_action :authorize_update_build!,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index dbcbd2467b6..f72f0ccb593 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -47,6 +47,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:single_file_file_by_file, project)
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:realtime_mr_status_change, project)
+ push_frontend_feature_flag(:realtime_approvals, project)
push_frontend_feature_flag(:saved_replies, current_user)
push_frontend_feature_flag(:code_quality_inline_drawer, project)
push_frontend_feature_flag(:hide_create_issue_resolve_all, project)
diff --git a/app/controllers/projects/metrics/dashboards/builder_controller.rb b/app/controllers/projects/metrics/dashboards/builder_controller.rb
index a6b57798923..02e3afcdc80 100644
--- a/app/controllers/projects/metrics/dashboards/builder_controller.rb
+++ b/app/controllers/projects/metrics/dashboards/builder_controller.rb
@@ -10,6 +10,8 @@ module Projects
urgency :low
def panel_preview
+ return not_found if Feature.enabled?(:remove_monitor_metrics)
+
respond_to do |format|
format.json do
if rendered_panel.success?
diff --git a/app/controllers/projects/performance_monitoring/dashboards_controller.rb b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
index d043f8d0b9f..1255ec1dde2 100644
--- a/app/controllers/projects/performance_monitoring/dashboards_controller.rb
+++ b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
@@ -16,6 +16,8 @@ module Projects
urgency :low
def create
+ return not_found if Feature.enabled?(:remove_monitor_metrics)
+
result = ::Metrics::Dashboard::CloneDashboardService.new(project, current_user, dashboard_params).execute
if result[:status] == :success
@@ -26,6 +28,8 @@ module Projects
end
def update
+ return not_found if Feature.enabled?(:remove_monitor_metrics)
+
result = ::Metrics::Dashboard::UpdateDashboardService.new(project, current_user, dashboard_params.merge(file_content_params)).execute
if result[:status] == :success
diff --git a/app/finders/pending_todos_finder.rb b/app/finders/pending_todos_finder.rb
index babff65cc37..a1a72840236 100644
--- a/app/finders/pending_todos_finder.rb
+++ b/app/finders/pending_todos_finder.rb
@@ -13,24 +13,36 @@
class PendingTodosFinder
attr_reader :users, :params
- # users - The list of users to retrieve the todos for.
+ # users - The list of users to retrieve the todos for. If nil is passed, it won't filter todos based on users
# params - A Hash containing columns and values to use for filtering todos.
- def initialize(users, params = {})
- @users = users
+ def initialize(params = {})
@params = params
+
+ # To prevent N+1 queries when fetching the users of the PendingTodos.
+ @preload_user_association = params.fetch(:preload_user_association, false)
end
def execute
- todos = Todo.for_user(users)
- todos = todos.pending
+ todos = Todo.pending
+ todos = by_users(todos)
todos = by_project(todos)
todos = by_target_id(todos)
todos = by_target_type(todos)
+ todos = by_author_id(todos)
todos = by_discussion(todos)
todos = by_commit_id(todos)
+
+ todos = todos.with_preloaded_user if @preload_user_association
+
by_action(todos)
end
+ def by_users(todos)
+ return todos unless params[:users].present?
+
+ todos.for_user(params[:users])
+ end
+
def by_project(todos)
if (id = params[:project_id])
todos.for_project(id)
@@ -55,6 +67,12 @@ class PendingTodosFinder
end
end
+ def by_author_id(todos)
+ return todos unless params[:author_id]
+
+ todos.for_author(params[:author_id])
+ end
+
def by_commit_id(todos)
if (id = params[:commit_id])
todos.for_commit(id)
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index 424e5920fed..a7e1de173bd 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -18,24 +18,6 @@ module Ci
}
end
- def bridge_data(build, project)
- {
- "build_id" => build.id,
- "empty-state-illustration-path" => image_path('illustrations/job-trigger-md.svg'),
- "pipeline_iid" => build.pipeline.iid,
- "project_full_path" => project.full_path
- }
- end
-
- def job_counts
- {
- "all" => limited_counter_with_delimiter(@all_builds),
- "pending" => limited_counter_with_delimiter(@all_builds.pending),
- "running" => limited_counter_with_delimiter(@all_builds.running),
- "finished" => limited_counter_with_delimiter(@all_builds.finished)
- }
- end
-
def job_statuses
statuses = Ci::HasStatus::AVAILABLE_STATUSES
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index fca6c76434a..d2ca88aae0e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -14,6 +14,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
ignore_column :send_user_confirmation_email, remove_with: '15.8', remove_after: '2022-12-18'
ignore_column :web_ide_clientside_preview_enabled, remove_with: '15.11', remove_after: '2023-04-22'
ignore_column :clickhouse_connection_string, remove_with: '16.1', remove_after: '2023-05-22'
+ ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -33,17 +34,13 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }, _prefix: true
- add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
+ add_authentication_token_field :runners_registration_token, encrypted: :required
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token, encrypted: :required
add_authentication_token_field :error_tracking_access_token, encrypted: :required
belongs_to :push_rule
- belongs_to :instance_group, class_name: "Group", foreign_key: :instance_administrators_group_id,
- inverse_of: :application_setting
- alias_attribute :instance_group_id, :instance_administrators_group_id
- alias_attribute :instance_administrators_group, :instance_group
alias_attribute :housekeeping_optimize_repository_period, :housekeeping_incremental_repack_period
sanitizes! :default_branch_name
diff --git a/app/models/group.rb b/app/models/group.rb
index f13ce2ddca1..ab8e0101684 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -108,8 +108,6 @@ class Group < Namespace
has_one :import_state, class_name: 'GroupImportState', inverse_of: :group
- has_many :application_setting, foreign_key: :instance_administrators_group_id, inverse_of: :instance_group
-
has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group
has_many :bulk_import_entities, class_name: 'BulkImports::Entity', foreign_key: :namespace_id, inverse_of: :group
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b0535a28640..0d33c6a71aa 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -684,13 +684,13 @@ class Issue < ApplicationRecord
elsif project.personal? && project.team.owner?(user)
true
elsif confidential? && !assignee_or_author?(user)
- project.team.member?(user, Gitlab::Access::REPORTER)
+ project.member?(user, Gitlab::Access::REPORTER)
elsif hidden?
false
elsif project.public? || (project.internal? && !user.external?)
project.feature_available?(:issues, user)
else
- project.team.member?(user)
+ project.member?(user)
end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 596186276bb..2ea71bfcd6d 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -92,7 +92,7 @@ class Key < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def update_last_used_at
- Keys::LastUsedService.new(self).execute
+ Keys::LastUsedService.new(self).execute_async
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index a3946724fd3..c58ad92d7a6 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -297,9 +297,14 @@ class Packages::Package < ApplicationRecord
end
# Technical debt: to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/281937
+ # TODO: rename the method https://gitlab.com/gitlab-org/gitlab/-/issues/410352
def original_build_info
strong_memoize(:original_build_info) do
- build_infos.first
+ if Feature.enabled?(:packages_display_last_pipeline, project)
+ build_infos.last
+ else
+ build_infos.first
+ end
end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 52e623db7b0..772a82fa173 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -206,7 +206,7 @@ class ProjectFeature < ApplicationRecord
override :resource_member?
def resource_member?(user, feature)
- project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
+ project.member?(user, ProjectFeature.required_minimum_access_level(feature))
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 181130d45bd..3c40f4beedc 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -157,7 +157,7 @@ class Snippet < ApplicationRecord
def for_project_with_user(project, user = nil)
return none unless project.snippets_visible?(user)
- if project.team.member?(user)
+ if project.member?(user)
project.snippets
else
project.snippets.public_to_user(user)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index ac41b5d0b2c..e1b5076e3d8 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -13,6 +13,7 @@ class Todo < ApplicationRecord
# and giving it back again.
WAIT_FOR_DELETE = 1.hour
+ # Actions
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
@@ -80,6 +81,7 @@ class Todo < ApplicationRecord
end
scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
scope :for_internal_notes, -> { joins(:note).where(note: { confidential: true }) }
+ scope :with_preloaded_user, -> { preload(:user) }
enum resolved_by_action: { system_done: 0, api_all_done: 1, api_done: 2, mark_all_done: 3, mark_done: 4 }, _prefix: :resolved_by
diff --git a/app/models/user.rb b/app/models/user.rb
index 71c5afb1c79..daee2687d2f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -80,14 +80,14 @@ class User < ApplicationRecord
algorithm: 'aes-256-cbc'
devise :two_factor_authenticatable,
- otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
+ otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
devise :two_factor_backupable, otp_number_of_backup_codes: 10
devise :two_factor_backupable_pbkdf2
serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable,
- :validatable, :omniauthable, :confirmable, :registerable
+ :validatable, :omniauthable, :confirmable, :registerable
# Must be included after `devise`
include EncryptedUserPassword
@@ -132,11 +132,11 @@ class User < ApplicationRecord
# Namespace for personal projects
has_one :namespace,
- -> { where(type: Namespaces::UserNamespace.sti_name) },
- dependent: :destroy, # rubocop:disable Cop/ActiveRecordDependent
- foreign_key: :owner_id,
- inverse_of: :owner,
- autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ -> { where(type: Namespaces::UserNamespace.sti_name) },
+ dependent: :destroy, # rubocop:disable Cop/ActiveRecordDependent
+ foreign_key: :owner_id,
+ inverse_of: :owner,
+ autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -171,18 +171,18 @@ class User < ApplicationRecord
has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group
has_many :developer_groups, -> { where(members: { access_level: ::Gitlab::Access::DEVELOPER }) }, through: :group_members, source: :group
has_many :owned_or_maintainers_groups,
- -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
- through: :group_members,
- source: :group
+ -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
+ through: :group_members,
+ source: :group
alias_attribute :masters_groups, :maintainers_groups
has_many :developer_maintainer_owned_groups,
- -> { where(members: { access_level: [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
- through: :group_members,
- source: :group
+ -> { where(members: { access_level: [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
+ through: :group_members,
+ source: :group
has_many :reporter_developer_maintainer_owned_groups,
- -> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
- through: :group_members,
- source: :group
+ -> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
+ through: :group_members,
+ source: :group
has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, class_name: 'GroupMember'
has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
@@ -349,27 +349,27 @@ class User < ApplicationRecord
enum role: { software_developer: 0, development_team_lead: 1, devops_engineer: 2, systems_administrator: 3, security_analyst: 4, data_analyst: 5, product_manager: 6, product_designer: 7, other: 8 }, _suffix: true
delegate :notes_filter_for,
- :set_notes_filter,
- :first_day_of_week, :first_day_of_week=,
- :timezone, :timezone=,
- :time_display_relative, :time_display_relative=,
- :show_whitespace_in_diffs, :show_whitespace_in_diffs=,
- :view_diffs_file_by_file, :view_diffs_file_by_file=,
- :pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=,
- :tab_width, :tab_width=,
- :sourcegraph_enabled, :sourcegraph_enabled=,
- :gitpod_enabled, :gitpod_enabled=,
- :setup_for_company, :setup_for_company=,
- :render_whitespace_in_code, :render_whitespace_in_code=,
- :markdown_surround_selection, :markdown_surround_selection=,
- :markdown_automatic_lists, :markdown_automatic_lists=,
- :diffs_deletion_color, :diffs_deletion_color=,
- :diffs_addition_color, :diffs_addition_color=,
- :use_new_navigation, :use_new_navigation=,
- :pinned_nav_items, :pinned_nav_items=,
- :achievements_enabled, :achievements_enabled=,
- :enabled_following, :enabled_following=,
- to: :user_preference
+ :set_notes_filter,
+ :first_day_of_week, :first_day_of_week=,
+ :timezone, :timezone=,
+ :time_display_relative, :time_display_relative=,
+ :show_whitespace_in_diffs, :show_whitespace_in_diffs=,
+ :view_diffs_file_by_file, :view_diffs_file_by_file=,
+ :pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=,
+ :tab_width, :tab_width=,
+ :sourcegraph_enabled, :sourcegraph_enabled=,
+ :gitpod_enabled, :gitpod_enabled=,
+ :setup_for_company, :setup_for_company=,
+ :render_whitespace_in_code, :render_whitespace_in_code=,
+ :markdown_surround_selection, :markdown_surround_selection=,
+ :markdown_automatic_lists, :markdown_automatic_lists=,
+ :diffs_deletion_color, :diffs_deletion_color=,
+ :diffs_addition_color, :diffs_addition_color=,
+ :use_new_navigation, :use_new_navigation=,
+ :pinned_nav_items, :pinned_nav_items=,
+ :achievements_enabled, :achievements_enabled=,
+ :enabled_following, :enabled_following=,
+ to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true
delegate :job_title, :job_title=, to: :user_detail, allow_nil: true
@@ -517,28 +517,27 @@ class User < ApplicationRecord
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
- where('EXISTS (?)',
- ::PersonalAccessToken
- .where('personal_access_tokens.user_id = users.id')
- .without_impersonation
- .expiring_and_not_notified(at).select(1))
+ where('EXISTS (?)', ::PersonalAccessToken
+ .where('personal_access_tokens.user_id = users.id')
+ .without_impersonation
+ .expiring_and_not_notified(at).select(1)
+ )
end
scope :with_personal_access_tokens_expired_today, -> do
- where('EXISTS (?)',
- ::PersonalAccessToken
- .select(1)
- .where('personal_access_tokens.user_id = users.id')
- .without_impersonation
- .expired_today_and_not_notified)
+ where('EXISTS (?)', ::PersonalAccessToken
+ .select(1)
+ .where('personal_access_tokens.user_id = users.id')
+ .without_impersonation
+ .expired_today_and_not_notified
+ )
end
scope :with_ssh_key_expiring_soon, -> do
includes(:expiring_soon_and_unnotified_keys)
- .where('EXISTS (?)',
- ::Key
- .select(1)
- .where('keys.user_id = users.id')
- .expiring_soon_and_not_notified)
+ .where('EXISTS (?)', ::Key
+ .select(1)
+ .where('keys.user_id = users.id')
+ .expiring_soon_and_not_notified)
end
scope :order_recent_sign_in, -> { reorder(arel_table[:current_sign_in_at].desc.nulls_last) }
scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) }
@@ -2057,9 +2056,11 @@ class User < ApplicationRecord
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_project_ids(project_ids)
- Gitlab::SafeRequestLoader.execute(resource_key: max_member_access_for_resource_key(Project),
- resource_ids: project_ids,
- default_value: Gitlab::Access::NO_ACCESS) do |project_ids|
+ Gitlab::SafeRequestLoader.execute(
+ resource_key: max_member_access_for_resource_key(Project),
+ resource_ids: project_ids,
+ default_value: Gitlab::Access::NO_ACCESS
+ ) do |project_ids|
project_authorizations.where(project: project_ids)
.group(:project_id)
.maximum(:access_level)
@@ -2074,9 +2075,11 @@ class User < ApplicationRecord
#
# Returns a Hash mapping project ID -> maximum access level.
def max_member_access_for_group_ids(group_ids)
- Gitlab::SafeRequestLoader.execute(resource_key: max_member_access_for_resource_key(Group),
- resource_ids: group_ids,
- default_value: Gitlab::Access::NO_ACCESS) do |group_ids|
+ Gitlab::SafeRequestLoader.execute(
+ resource_key: max_member_access_for_resource_key(Group),
+ resource_ids: group_ids,
+ default_value: Gitlab::Access::NO_ACCESS
+ ) do |group_ids|
group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
end
end
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 3a1fce9c0b4..9c40376bb13 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -20,8 +20,8 @@ class UserPreference < ApplicationRecord
less_than_or_equal_to: Gitlab::TabWidth::MAX
}
validates :diffs_deletion_color, :diffs_addition_color,
- format: { with: ColorsHelper::HEX_COLOR_PATTERN },
- allow_blank: true
+ format: { with: ColorsHelper::HEX_COLOR_PATTERN },
+ allow_blank: true
validates :pass_user_identities_to_ci_jwt, allow_nil: false, inclusion: { in: [true, false] }
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 601622b8c4c..1cc9f1f50ad 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -30,9 +30,9 @@ module Users
validates :group, presence: true
validates :feature_name,
- presence: true,
- uniqueness: { scope: [:user_id, :group_id] },
- inclusion: { in: GroupCallout.feature_names.keys }
+ presence: true,
+ uniqueness: { scope: [:user_id, :group_id] },
+ inclusion: { in: GroupCallout.feature_names.keys }
def source_feature_name
"#{feature_name}_#{group_id}"
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index b9e4e908ddd..52f16a7861f 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -8,28 +8,25 @@ module Users
belongs_to :user, foreign_key: :user_id
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id
- validates :country,
- presence: true,
- length: { maximum: 3 }
+ validates :country, presence: true, length: { maximum: 3 }
validates :international_dial_code,
- presence: true,
- numericality: {
- only_integer: true,
- greater_than_or_equal_to: 1,
- less_than_or_equal_to: 999
- }
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than_or_equal_to: 1,
+ less_than_or_equal_to: 999
+ }
validates :phone_number,
- presence: true,
- format: {
- with: /\A\d+\Z/,
- message: -> (object, data) { _('can contain only digits') }
- },
- length: { maximum: 12 }
-
- validates :telesign_reference_xid,
- length: { maximum: 255 }
+ presence: true,
+ format: {
+ with: /\A\d+\Z/,
+ message: -> (object, data) { _('can contain only digits') }
+ },
+ length: { maximum: 12 }
+
+ validates :telesign_reference_xid, length: { maximum: 255 }
scope :for_user, -> (user_id) { where(user_id: user_id) }
diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb
index 6199c3697f7..3964f202be6 100644
--- a/app/models/users/project_callout.rb
+++ b/app/models/users/project_callout.rb
@@ -20,8 +20,8 @@ module Users
validates :project, presence: true
validates :feature_name,
- presence: true,
- uniqueness: { scope: [:user_id, :project_id] },
- inclusion: { in: ProjectCallout.feature_names.keys }
+ presence: true,
+ uniqueness: { scope: [:user_id, :project_id] },
+ inclusion: { in: ProjectCallout.feature_names.keys }
end
end
diff --git a/app/models/users/user_follow_user.rb b/app/models/users/user_follow_user.rb
index 5a82a81364a..c9d4bee496c 100644
--- a/app/models/users/user_follow_user.rb
+++ b/app/models/users/user_follow_user.rb
@@ -14,9 +14,13 @@ module Users
followee_count = self.class.where(follower_id: follower_id).limit(MAX_FOLLOWEE_LIMIT).count
return if followee_count < MAX_FOLLOWEE_LIMIT
- errors.add(:base, format(
- _("You can't follow more than %{limit} users. To follow more users, unfollow some others."),
- limit: MAX_FOLLOWEE_LIMIT))
+ errors.add(
+ :base,
+ format(
+ _("You can't follow more than %{limit} users. To follow more users, unfollow some others."),
+ limit: MAX_FOLLOWEE_LIMIT
+ )
+ )
end
end
end
diff --git a/app/models/web_ide_terminal.rb b/app/models/web_ide_terminal.rb
index ef70df2405f..fe69ca80c32 100644
--- a/app/models/web_ide_terminal.rb
+++ b/app/models/web_ide_terminal.rb
@@ -39,12 +39,14 @@ class WebIdeTerminal
private
def web_ide_terminal_route_generator(action, options = {})
- options.reverse_merge!(action: action,
- controller: 'projects/web_ide_terminals',
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: build.id,
- only_path: true)
+ options.reverse_merge!(
+ action: action,
+ controller: 'projects/web_ide_terminals',
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: build.id,
+ only_path: true
+ )
url_for(options)
end
diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb
index 86e7e2dedc3..c8b2513e702 100644
--- a/app/models/webauthn_registration.rb
+++ b/app/models/webauthn_registration.rb
@@ -12,5 +12,5 @@ class WebauthnRegistration < ApplicationRecord
validates :credential_xid, :public_key, :counter, presence: true
validates :name, length: { minimum: 0, allow_nil: false }
validates :counter,
- numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index b04aa196883..e1468872f52 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -145,10 +145,12 @@ class WikiPage
default_per_page = Kaminari.config.default_per_page
offset = [options[:page].to_i - 1, 0].max * options.fetch(:per_page, default_per_page)
- wiki.repository.commits(wiki.default_branch,
- path: page.path,
- limit: options.fetch(:limit, default_per_page),
- offset: offset)
+ wiki.repository.commits(
+ wiki.default_branch,
+ path: page.path,
+ limit: options.fetch(:limit, default_per_page),
+ offset: offset
+ )
end
def count_versions
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 4724d644318..24d1078516e 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -16,10 +16,10 @@ class WorkItem < Issue
has_many :child_links, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_parent_id
has_many :work_item_children, through: :child_links, class_name: 'WorkItem',
- foreign_key: :work_item_id, source: :work_item
+ foreign_key: :work_item_id, source: :work_item
has_many :work_item_children_by_relative_position, -> { work_item_children_keyset_order },
- through: :child_links, class_name: 'WorkItem',
- foreign_key: :work_item_id, source: :work_item
+ through: :child_links, class_name: 'WorkItem',
+ foreign_key: :work_item_id, source: :work_item
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
diff --git a/app/services/keys/last_used_service.rb b/app/services/keys/last_used_service.rb
index daef544bac0..3683c03b7a4 100644
--- a/app/services/keys/last_used_service.rb
+++ b/app/services/keys/last_used_service.rb
@@ -2,7 +2,7 @@
module Keys
class LastUsedService
- TIMEOUT = 1.day.to_i
+ TIMEOUT = 1.day
attr_reader :key
@@ -12,26 +12,24 @@ module Keys
end
def execute
+ return unless update?
+
# We _only_ want to update last_used_at and not also updated_at (which
# would be updated when using #touch).
- key.update_column(:last_used_at, Time.zone.now) if update?
+ key.update_column(:last_used_at, Time.zone.now)
end
- def update?
- return false if ::Gitlab::Database.read_only?
-
- last_used = key.last_used_at
+ def execute_async
+ return unless update?
- return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
-
- !!redis_lease.try_obtain
+ ::SshKeys::UpdateLastUsedAtWorker.perform_async(key.id)
end
- private
+ def update?
+ return false if ::Gitlab::Database.read_only?
- def redis_lease
- Gitlab::ExclusiveLease
- .new("key_update_last_used_at:#{key.id}", timeout: TIMEOUT)
+ last_used = key.last_used_at
+ last_used.blank? || last_used <= TIMEOUT.ago
end
end
end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 20f96ac2949..f8c91fbae7d 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -18,7 +18,7 @@ module Members
def after_execute(member:, skip_log_audit_event:)
super
- resolve_access_request_todos(current_user, member)
+ resolve_access_request_todos(member)
end
def validate_access!(access_requester)
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
index 801f77ae082..80fba33b20e 100644
--- a/app/services/members/base_service.rb
+++ b/app/services/members/base_service.rb
@@ -53,8 +53,8 @@ module Members
end
end
- def resolve_access_request_todos(current_user, requester)
- todo_service.resolve_access_request_todos(current_user, requester)
+ def resolve_access_request_todos(member)
+ todo_service.resolve_access_request_todos(member)
end
def enqueue_delete_todos(member)
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 0f195663a61..b77485ce744 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -84,7 +84,7 @@ module Members
delete_subresources(member) unless skip_subresources
delete_project_invitations_by(member) unless skip_subresources
- resolve_access_request_todos(current_user, member)
+ resolve_access_request_todos(member)
after_execute(member: member)
end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index b373a099020..d31f4596fa5 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -26,7 +26,7 @@ module Projects
def user_is_at_least_reporter?
strong_memoize(:user_is_at_least_reporter) do
- @project.team.member?(@user, Gitlab::Access::REPORTER)
+ @project.member?(@user, Gitlab::Access::REPORTER)
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 2025d438ae7..c55e1680bfe 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -175,13 +175,26 @@ class TodoService
TodosFinder.new(current_user).any_for_target?(issuable, :pending)
end
- # Resolves all todos related to target
+ # Resolves all todos related to target for the current_user
def resolve_todos_for_target(target, current_user)
attributes = attributes_for_target(target)
resolve_todos(pending_todos([current_user], attributes), current_user)
end
+ # Resolves all todos related to target for all users
+ def resolve_todos_with_attributes_for_target(target, attributes, resolution: :done, resolved_by_action: :system_done)
+ target_attributes = { target_id: target.id, target_type: target.class.polymorphic_name }
+ attributes.merge!(target_attributes)
+ attributes[:preload_user_association] = true
+
+ todos = PendingTodosFinder.new(attributes).execute
+ users = todos.map(&:user)
+ todos_ids = todos.batch_update(state: resolution, resolved_by_action: resolved_by_action)
+ users.each(&:update_todos_count_cache)
+ todos_ids
+ end
+
def resolve_todos(todos, current_user, resolution: :done, resolved_by_action: :system_done)
todos_ids = todos.batch_update(state: resolution, resolved_by_action: resolved_by_action)
@@ -198,21 +211,20 @@ class TodoService
current_user.update_todos_count_cache
end
- def resolve_access_request_todos(current_user, member)
- return if current_user.nil? || member.nil?
+ def resolve_access_request_todos(member)
+ return if member.nil?
+ # Group or Project
target = member.source
- finder_params = {
+ todos_params = {
state: :pending,
author_id: member.user_id,
- action_id: ::Todo::MEMBER_ACCESS_REQUESTED,
- type: target.class.polymorphic_name,
- target: target.id
+ action: ::Todo::MEMBER_ACCESS_REQUESTED,
+ type: target.class.polymorphic_name
}
- todos = TodosFinder.new(current_user, finder_params).execute
- resolve_todos(todos, current_user)
+ resolve_todos_with_attributes_for_target(target, todos_params)
end
def restore_todos(todos, current_user)
@@ -419,7 +431,7 @@ class TodoService
end
def pending_todos(users, criteria = {})
- PendingTodosFinder.new(users, criteria).execute
+ PendingTodosFinder.new(criteria.merge(users: users)).execute
end
def track_todo_creation(user, issue_type, namespace, project)
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index f53b2051835..7c936c849d0 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -4,7 +4,7 @@
- if @snippets.exists?
- if current_user
.top-area
- - include_private = @project.team.member?(current_user) || current_user.admin?
+ - include_private = @project.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private, counts: @snippet_counts }
- if new_project_snippet_link.present?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e9965ff0027..d6f410f4b1d 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3468,6 +3468,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: ssh_keys_update_last_used_at
+ :worker_name: SshKeys::UpdateLastUsedAtWorker
+ :feature_category: :source_code_management
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: system_hook_push
:worker_name: SystemHookPushWorker
:feature_category: :source_code_management
diff --git a/app/workers/ssh_keys/update_last_used_at_worker.rb b/app/workers/ssh_keys/update_last_used_at_worker.rb
new file mode 100644
index 00000000000..80c2132b8d8
--- /dev/null
+++ b/app/workers/ssh_keys/update_last_used_at_worker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module SshKeys
+ class UpdateLastUsedAtWorker
+ include ApplicationWorker
+
+ idempotent!
+ deduplicate :until_executed
+ data_consistency :sticky
+
+ feature_category :source_code_management
+
+ def perform(key_id)
+ key = Key.find_by_id(key_id)
+
+ return unless key
+
+ Keys::LastUsedService.new(key).execute
+ end
+ end
+end
diff --git a/config/feature_flags/development/anthropic_experimentation.yml b/config/feature_flags/development/anthropic_experimentation.yml
new file mode 100644
index 00000000000..8d4f2200532
--- /dev/null
+++ b/config/feature_flags/development/anthropic_experimentation.yml
@@ -0,0 +1,8 @@
+---
+name: anthropic_experimentation
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119729
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/409939
+milestone: '16.0'
+type: development
+group: group::ai-enablement
+default_enabled: false
diff --git a/config/feature_flags/development/application_settings_tokens_optional_encryption.yml b/config/feature_flags/development/application_settings_tokens_optional_encryption.yml
deleted file mode 100644
index c3619dbe2d0..00000000000
--- a/config/feature_flags/development/application_settings_tokens_optional_encryption.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: application_settings_tokens_optional_encryption
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25532
-rollout_issue_url:
-milestone: '11.9'
-type: development
-group: group::runner
-default_enabled: false
diff --git a/config/feature_flags/development/expose_authorized_cluster_agents.yml b/config/feature_flags/development/expose_authorized_cluster_agents.yml
index 9110c2be6c6..b5a44d5d309 100644
--- a/config/feature_flags/development/expose_authorized_cluster_agents.yml
+++ b/config/feature_flags/development/expose_authorized_cluster_agents.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407841
milestone: '16.0'
type: development
group: group::environments
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/groups_tokens_optional_encryption.yml b/config/feature_flags/development/groups_tokens_optional_encryption.yml
index c34cae689c2..4f67ea4bd6c 100644
--- a/config/feature_flags/development/groups_tokens_optional_encryption.yml
+++ b/config/feature_flags/development/groups_tokens_optional_encryption.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333862
milestone: '11.9'
type: development
group: group::runner
-default_enabled: true
+default_enabled: false
diff --git a/config/feature_flags/development/packages_display_last_pipeline.yml b/config/feature_flags/development/packages_display_last_pipeline.yml
new file mode 100644
index 00000000000..a90edb4bb86
--- /dev/null
+++ b/config/feature_flags/development/packages_display_last_pipeline.yml
@@ -0,0 +1,8 @@
+---
+name: packages_display_last_pipeline
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120125
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410362
+milestone: '16.0'
+type: development
+group: group::package registry
+default_enabled: false
diff --git a/config/feature_flags/development/projects_tokens_optional_encryption.yml b/config/feature_flags/development/projects_tokens_optional_encryption.yml
index 6a0fed009ea..c35becd87ab 100644
--- a/config/feature_flags/development/projects_tokens_optional_encryption.yml
+++ b/config/feature_flags/development/projects_tokens_optional_encryption.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333864
milestone: '11.9'
type: development
group: group::runner
-default_enabled: true
+default_enabled: false
diff --git a/config/feature_flags/development/realtime_approvals.yml b/config/feature_flags/development/realtime_approvals.yml
new file mode 100644
index 00000000000..b2a4131b0f6
--- /dev/null
+++ b/config/feature_flags/development/realtime_approvals.yml
@@ -0,0 +1,8 @@
+---
+name: realtime_approvals
+introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114963'
+rollout_issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/399539'
+milestone: '16.0'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/metrics/counts_7d/20230509085219_i_quickactions_blocked_by_weekly.yml b/config/metrics/counts_7d/20230509085219_i_quickactions_blocked_by_weekly.yml
new file mode 100644
index 00000000000..d4406527dbd
--- /dev/null
+++ b/config/metrics/counts_7d/20230509085219_i_quickactions_blocked_by_weekly.yml
@@ -0,0 +1,23 @@
+---
+key_path: redis_hll_counters.quickactions.i_quickactions_blocked_by_weekly
+name: quickactions_blocked_by_weekly
+description: Count of MAU using the `/blocked_by` quick action
+product_section: dev
+product_stage: plan
+product_group: product_planning
+value_type: number
+status: active
+milestone: "16.0"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120006
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_blocked_by
+performance_indicator_type: []
+distribution:
+- ee
+tier:
+- starter
diff --git a/config/metrics/counts_7d/20230509090906_i_quickactions_blocks_weekly.yml b/config/metrics/counts_7d/20230509090906_i_quickactions_blocks_weekly.yml
new file mode 100644
index 00000000000..3d5b8ed6ce2
--- /dev/null
+++ b/config/metrics/counts_7d/20230509090906_i_quickactions_blocks_weekly.yml
@@ -0,0 +1,23 @@
+---
+key_path: redis_hll_counters.quickactions.i_quickactions_blocks_weekly
+name: quickactions_blocks_weekly
+description: Count of MAU using the `/blocks` quick action
+product_section: dev
+product_stage: plan
+product_group: product_planning
+value_type: number
+status: active
+milestone: "16.0"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120006
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - i_quickactions_blocks
+performance_indicator_type: []
+distribution:
+- ee
+tier:
+- starter
diff --git a/config/routes.rb b/config/routes.rb
index 9a5fb2df4c5..9c8ad8fe047 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -168,7 +168,6 @@ InitializerConnections.raise_if_new_database_connection do
draw :country
draw :country_state
draw :subscription
- draw :llm
scope '/push_from_secondary/:geo_node_id' do
draw :git_http
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 50511d35c0a..0d138c58ff2 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -539,6 +539,8 @@
- 1
- - snippets_update_repository_storage
- 1
+- - ssh_keys_update_last_used_at
+ - 1
- - status_page_publish
- 1
- - sync_seat_link_request
diff --git a/data/removals/16_0/16-0-ci-builds-column-validations.yml b/data/removals/16_0/16-0-ci-builds-column-validations.yml
new file mode 100644
index 00000000000..7b8bf455ef0
--- /dev/null
+++ b/data/removals/16_0/16-0-ci-builds-column-validations.yml
@@ -0,0 +1,24 @@
+- title: "Enforced validation of CI/CD parameter character lengths" # (required) Clearly explain the change. For example, "The `confidential` field for a `Note` is removed" or "CI/CD job names are limited to 250 characters."
+ announcement_milestone: "15.9" # (required) The milestone when this feature was deprecated.
+ removal_milestone: "16.0" # (required) The milestone when this feature is being removed.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: jreporter # (required) GitLab username of the person reporting the removal
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372770 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ Previously, only CI/CD [job names](https://docs.gitlab.com/ee/ci/jobs/index.html#job-name-limitations) had a strict 255-character limit. Now, more CI/CD keywords are validated to ensure they stay under the limit.
+
+ The following to 255 characters are now strictly limited to 255 characters:
+
+ - The `stage` keyword.
+ - The `ref` parameter, which is the Git branch or tag name for the pipeline.
+ - The `description` and `target_url` parameters, used by external CI/CD integrations.
+
+ Users on self-managed instances should update their pipelines to ensure they do not use parameters that exceed 255 characters. Users on GitLab.com do not need to make any changes, as these parameters are already limited in that database.
+
+# OPTIONAL FIELDS
+#
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/data/removals/16_0/16-0-grafana-chart.yml b/data/removals/16_0/16-0-grafana-chart.yml
new file mode 100644
index 00000000000..012d5043a98
--- /dev/null
+++ b/data/removals/16_0/16-0-grafana-chart.yml
@@ -0,0 +1,19 @@
+- title: "Bundled Grafana Helm Chart"
+ announcement_milestone: "15.10"
+ removal_milestone: "16.0"
+ breaking_change: true
+ reporter: twk3
+ stage: Enablement
+ issue_url: https://gitlab.com/gitlab-org/charts/gitlab/-/issues/4353
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The Grafana Helm chart that was bundled with the GitLab Helm Chart is removed in the GitLab Helm Chart 7.0 release (releasing along with GitLab 16.0).
+
+ The `global.grafana.enabled` setting for the GitLab Helm Chart has also been removed alongside the Grafana Helm chart.
+
+ If you're using the bundled Grafana, you should switch to the [newer chart version from Grafana Labs](https://artifacthub.io/packages/helm/grafana/grafana)
+ or a Grafana Operator from a trusted provider.
+
+ In your new Grafana instance, you can [configure the GitLab provided Prometheus as a data source](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html#integration-with-gitlab-ui)
+ and [connect Grafana to the GitLab UI](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html#integration-with-gitlab-ui).
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html
diff --git a/data/removals/16_0/16-0-limit-ci-job-token.yml b/data/removals/16_0/16-0-limit-ci-job-token.yml
new file mode 100644
index 00000000000..9f262f9c772
--- /dev/null
+++ b/data/removals/16_0/16-0-limit-ci-job-token.yml
@@ -0,0 +1,32 @@
+- title: "Limit CI_JOB_TOKEN scope is disabled" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
+ announcement_milestone: "15.9" # (required) The milestone when this feature was first announced as deprecated.
+ removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
+ breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
+ reporter: jreporter # (required) GitLab username of the person reporting the deprecation
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395708 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ In GitLab 14.4 we introduced the ability to [limit your project's CI/CD job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#limit-your-projects-job-token-access) (`CI_JOB_TOKEN`) access to make it more secure. You could use the **Limit CI_JOB_TOKEN access** setting to prevent job tokens from your project's pipelines from being used to **access other projects**. When enabled with no other configuration, your pipelines could not access any other projects. To use job tokens to access other projects from your project's pipelines, you needed to list those other projects explicitly in the setting's allowlist, and you needed to be a maintainer in _all_ the projects. You might have seen this mentioned as the "outbound scope" of the job token.
+
+ The job token functionality was updated in 15.9 with a [better security setting](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-access-to-your-project-with-a-job-token). Instead of securing your own project's job tokens from accessing other projects, the new workflow is to secure your own project from being accessed by other projects' job tokens without authorization. You can see this as an "inbound scope" for job tokens. When this new **Allow access to this project with a CI_JOB_TOKEN** setting is enabled with no other configuration, job tokens from other projects cannot **access your project**. If you want a project to have access to your own project, you must list it in the new setting's allowlist. You must be a maintainer in your own project to control the new allowlist, but you only need to have the Guest role in the other projects. This new setting is enabled by default for all new projects.
+
+ In GitLab 16.0, the old **Limit CI_JOB_TOKEN access** setting is disabled by default for all **new** projects. In existing projects with this setting currently enabled, it will continue to function as expected, but you are unable to add any more projects to the old allowlist. If the setting is disabled in any project, it is not possible to re-enable this setting in 16.0 or later. To control access between your projects, use the new **Allow access** setting instead.
+
+ In 17.0, we plan to remove the **Limit** setting completely, and set the **Allow access** setting to enabled for all projects. This change ensures a higher level of security between projects. If you currently use the **Limit** setting, you should update your projects to use the **Allow access** setting instead. If other projects access your project with a job token, you must add them to the **Allow access** setting's allowlist.
+
+ To prepare for this change, users on GitLab.com or self-managed GitLab 15.9 or later can enable the **Allow access** setting now and add the other projects. It will not be possible to disable the setting in 17.0 or later.
+ #
+ # OPTIONAL END OF SUPPORT FIELDS
+ #
+ # If an End of Support period applies, the announcement should be shared with GitLab Support
+ # in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
+ #
+ end_of_support_milestone: # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
+ end_of_support_date: # (optional) The date of the milestone release when support for this feature will end.
+ #
+ # OTHER OPTIONAL FIELDS
+ #
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: "https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#configure-the-job-token-scope-limit" # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/data/removals/16_0/16-0-pipeline_activity_limit.yml b/data/removals/16_0/16-0-pipeline_activity_limit.yml
new file mode 100644
index 00000000000..d8644b00deb
--- /dev/null
+++ b/data/removals/16_0/16-0-pipeline_activity_limit.yml
@@ -0,0 +1,12 @@
+- title: "Maximum number of active pipelines per project limit (`ci_active_pipelines`)" # (required) Clearly explain the change. For example, "The `confidential` field for a `Note` is removed" or "CI/CD job names are limited to 250 characters."
+ announcement_milestone: "15.3" # (required) The milestone when this feature was deprecated.
+ removal_milestone: "16.0" # (required) The milestone when this feature is being removed.
+ breaking_change: false # (required) Change to false if this is not a breaking change.
+ reporter: jreporter # (required) GitLab username of the person reporting the removal
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368195 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The [**Maximum number of active pipelines per project** limit](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#set-cicd-limits) has been removed. Instead, use the other recommended rate limits that offer similar protection:
+
+ - [**Pipelines rate limits**](https://docs.gitlab.com/ee/user/admin_area/settings/rate_limit_on_pipelines_creation.html).
+ - [**Total number of jobs in currently active pipelines**](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#set-cicd-limits).
diff --git a/data/removals/16_0/16-0-redis-config-env.yml b/data/removals/16_0/16-0-redis-config-env.yml
new file mode 100644
index 00000000000..956639edc1f
--- /dev/null
+++ b/data/removals/16_0/16-0-redis-config-env.yml
@@ -0,0 +1,13 @@
+- title: "Configuring Redis config file paths using environment variables is no longer supported"
+ announcement_milestone: "15.8"
+ removal_milestone: "16.0"
+ breaking_change: true
+ reporter: marcogreg
+ stage: platforms
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388255
+ body: |
+ You can no longer specify Redis configuration file locations
+ using the environment variables like `GITLAB_REDIS_CACHE_CONFIG_FILE` or
+ `GITLAB_REDIS_QUEUES_CONFIG_FILE`. Use the default
+ configuration file locations instead, for example `config/redis.cache.yml` or
+ `config/redis.queues.yml`.
diff --git a/data/removals/16_0/16-0-redis-localhost.yml b/data/removals/16_0/16-0-redis-localhost.yml
new file mode 100644
index 00000000000..1b15efe4749
--- /dev/null
+++ b/data/removals/16_0/16-0-redis-localhost.yml
@@ -0,0 +1,16 @@
+- title: "Non-standard default Redis ports are no longer supported"
+ announcement_milestone: "15.8"
+ removal_milestone: "16.0"
+ breaking_change: true
+ reporter: marcogreg
+ stage: platforms # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388269
+ body: |
+ If GitLab starts without any Redis configuration file present,
+ GitLab assumes it can connect to three Redis servers at `localhost:6380`,
+ `localhost:6381` and `localhost:6382`. We are changing this behavior
+ so GitLab assumes there is one Redis server at `localhost:6379`.
+
+ If you want to keep using the three servers, you must configure
+ the Redis URLs by editing the `config/redis.cache.yml`,`config/redis.queues.yml`,
+ and `config/redis.shared_state.yml` files.
diff --git a/data/removals/16_0/16-0-terraform-latest-stable-change.yml b/data/removals/16_0/16-0-terraform-latest-stable-change.yml
new file mode 100644
index 00000000000..0025a575a1d
--- /dev/null
+++ b/data/removals/16_0/16-0-terraform-latest-stable-change.yml
@@ -0,0 +1,21 @@
+- title: "The stable Terraform CI/CD template has been replaced with the latest template"
+ announcement_milestone: "15.8" # (required) The milestone when this feature was deprecated.
+ removal_milestone: "16.0" # (required) The milestone when this feature is being removed.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: timofurrer # (required) GitLab username of the person reporting the removal
+ stage: deploy # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/386001 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ With every major GitLab version, we update the stable Terraform templates with the current latest templates.
+ This change affects the [quickstart](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml)
+ and the [base](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml) templates.
+
+ The new templates do not change the directory to `$TF_ROOT` explicitly: `gitlab-terraform` gracefully
+ handles directory changing. If you altered the job scripts to assume that the current working directory is `$TF_ROOT`, you must manually add `cd "$TF_ROOT"` now.
+
+ Because the latest template introduces Merge Request Pipeline support which is not supported in Auto DevOps,
+ those rules are not yet integrated into the stable template.
+ However, we may introduce them later on, which may break your Terraform pipelines in regards to which jobs are executed.
+
+ To accommodate the changes, you might need to adjust the [`rules`](https://docs.gitlab.com/ee/ci/yaml/#rules) in your
+ `.gitlab-ci.yml` file.
diff --git a/doc/api/graphql/removed_items.md b/doc/api/graphql/removed_items.md
index 3b9526dc283..fb4a1a80340 100644
--- a/doc/api/graphql/removed_items.md
+++ b/doc/api/graphql/removed_items.md
@@ -21,6 +21,8 @@ Fields removed in GitLab 16.0.
| `name` | `PipelineSecurityReportFinding` | [15.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89571) | [!119055](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119055) | `title` |
| `external` | `ReleaseAssetLink` | 15.9 | [!111750](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111750) | None |
| `confidence` | `PipelineSecurityReportFinding` | 15.4 | [!118617](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118617) | None |
+| `PAUSED` | `CiRunnerStatus` | 14.8 | [!118635](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118635) | `CiRunner.paused: true` |
+| `ACTIVE` | `CiRunnerStatus` | 14.8 | [!118635](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118635) | `CiRunner.paused: false` |
### GraphQL Mutations
diff --git a/doc/api/rest/deprecations.md b/doc/api/rest/deprecations.md
index 295f869720c..db9b590606f 100644
--- a/doc/api/rest/deprecations.md
+++ b/doc/api/rest/deprecations.md
@@ -12,6 +12,14 @@ The date of this change is unknown.
For details, see [issue 216456](https://gitlab.com/gitlab-org/gitlab/-/issues/216456)
and [issue 387485](https://gitlab.com/gitlab-org/gitlab/-/issues/387485).
+## `geo_nodes` API endpoints
+
+Breaking change. [Related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/369140).
+
+The [`geo_nodes` API endpoints](../geo_nodes.md) are deprecated and are replaced by [`geo_sites`](../geo_sites.md).
+It is a part of the global change on [how to refer to Geo deployments](../../administration/geo/glossary.md).
+Nodes are renamed to sites across the application. The functionality of both endpoints remains the same.
+
## `merged_by` API field
Breaking change. [Related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/350534).
diff --git a/doc/api/users.md b/doc/api/users.md
index 76d6ccbc06e..f4547c06582 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -6,6 +6,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Users API **(FREE)**
+This documentation has information on API calls, parameters and responses for the Users API.
+
+For information on user activities that update the user event timestamps, see [get user activities](#get-user-activities).
+
## List users
Get a list of users.
@@ -2063,7 +2067,7 @@ Pre-requisite:
Get the last activity date for all users, sorted from oldest to newest.
-The activities that update the timestamp are:
+The activities that update the user event timestamps (`last_activity_on` and `current_sign_in_at`) are:
- Git HTTP/SSH activities (such as clone, push)
- User logging in to GitLab
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 0e70b97d2c7..0d3cc9c3e17 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -399,19 +399,21 @@ scope.
| GitLab Rails app | `%15.10` | Define feature flag and policies for "New Runner creation workflow" for groups and projects. |
| GitLab Rails app | `%15.10` | Only update runner `contacted_at` and `status` when polled for jobs. |
| GitLab Rails app | `%15.10` | Add GraphQL type to represent runner machines under `CiRunner`. |
-| GitLab Rails app | `%15.10` | Implement UI to create new instance runner. |
+| GitLab Rails app | `%15.11` | Implement UI to create new instance runner. |
| GitLab Rails app | `%15.11` | Update service and mutation to accept groups and projects. |
| GitLab Rails app | `%15.11` | Implement UI to create new group/project runners. |
-| GitLab Rails app | `%15.11` | GraphQL changes to `CiRunner` type. (?) |
+| GitLab Rails app | `%15.11` | Add runner_machine field to CiJob GraphQL type. |
| GitLab Rails app | `%15.11` | UI changes to runner details view (listing of platform, architecture, IP address, etc.) (?) |
| GitLab Rails app | `%15.11` | Adapt `POST /api/v4/runners` REST endpoint to accept a request from an authorized user with a scope instead of a registration token. |
-| GitLab Runner | `%15.11` | Handle glrt- runner tokens in `unregister` command. |
+| GitLab Runner | `%15.11` | Handle `glrt-` runner tokens in `unregister` command. |
+| GitLab Runner | `%15.11` | Runner asks for registration token when a `glrt-` runner token is passed in `--token`. |
+| GitLab Rails app | `%15.11` | Move from 'runner machine' terminology to 'runner manager'. |
### Stage 5 - Optional disabling of registration token
| Component | Milestone | Changes |
|------------------|----------:|---------|
-| GitLab Rails app | | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
+| GitLab Rails app | `%16.0` | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
| GitLab Rails app | | Add UI to allow disabling use of registration tokens at project or group level. |
| GitLab Rails app | | Introduce `:enforce_create_runner_workflow` feature flag (disabled by default) to control whether use of registration tokens is allowed. |
| GitLab Rails app | | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting or `:enforce_create_runner_workflow` feature flag disables registration tokens.<br/>A future v5 version of the API should return `HTTP 404 Not Found`. |
diff --git a/doc/development/ai_features.md b/doc/development/ai_features.md
index 11fe4f16e3b..e5632351d21 100644
--- a/doc/development/ai_features.md
+++ b/doc/development/ai_features.md
@@ -66,6 +66,16 @@ All AI features are experimental.
1. Enable the specific feature flag for the feature you want to test
1. Set either the required access token `OpenAi` or `Vertex`. Ask in [`#ai_enablement_team`](https://gitlab.slack.com/archives/C051K31F30R) to receive an access token.
+### Internal-Only GCP account access
+
+In order to obtain a GCP service key for local development, please follow the steps below:
+
+- Create a sandbox GCP environment by visiting [this page](https://about.gitlab.com/handbook/infrastructure-standards/#individual-environment) and following the instructions
+- In the GCP console, go to `IAM & Admin` > `Service Accounts` and click on the "Create new service account" button
+- Name the service account something specific to what you're using it for. Select Create and Continue. Under `Grant this service account access to project`, select the role `Vertex AI User`. Select `Continue` then `Done`
+- Select your new service account and `Manage keys` > `Add Key` > `Create new key`. This will download the **private** JSON credentials for your service account.
+- In the rails console, you will use this by `Gitlab::CurrentSettings.update(tofa_credentials: File.read('/YOUR_FILE.json'))`
+
## Experimental REST API
Use the [experimental REST API endpoints](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/experimentation/open_ai.rb) to quickly experiment and prototype AI features.
@@ -144,7 +154,7 @@ A[GitLab frontend] -->B[AiAction GraphQL mutation]
B --> C[Llm::ExecuteMethodService]
C --> D[One of services, for example: Llm::GenerateSummaryService]
D -->|scheduled| E[AI worker:Llm::CompletionWorker]
-E -->F[::Gitlab::Llm::OpenAi::Completions::Factory]
+E -->F[::Gitlab::Llm::Completions::Factory]
F -->G[`::Gitlab::Llm::OpenAi::Completions::...` class using `::Gitlab::Llm::OpenAi::Templates::...` class]
G -->|calling| H[Gitlab::Llm::OpenAi::Client]
H --> |response| I[::Gitlab::Llm::OpenAi::ResponseService]
@@ -152,6 +162,72 @@ I --> J[GraphqlTriggers.ai_completion_response]
J --> K[::GitlabSchema.subscriptions.trigger]
```
+## CircuitBreaker
+
+The CircuitBreaker concern is a reusable module that you can include in any class that needs to run code with circuit breaker protection. The concern provides a `run_with_circuit` method that wraps a code block with circuit breaker functionality, which helps prevent cascading failures and improves system resilience. For more information about the circuit breaker pattern, see:
+
+- [What is Circuit breaker](https://martinfowler.com/bliki/CircuitBreaker.html).
+- [The Hystrix documentation on CircuitBreaker](https://github.com/Netflix/Hystrix/wiki/How-it-Works#circuit-breaker).
+
+### Use CircuitBreaker
+
+To use the CircuitBreaker concern, you need to include it in a class and define the `service_name` method, which should return the name of the service that the circuit breaker is protecting. For example:
+
+```ruby
+class MyService
+ include Gitlab::Llm::Concerns::CircuitBreaker
+
+ def call_external_service
+ run_with_circuit do
+ # Code that interacts with external service goes here
+
+ raise MyCustomError
+ end
+ end
+
+ private
+
+ def service_name
+ my_service
+ end
+end
+```
+
+The `call_external_service` method is an example method that interacts with an external service.
+By wrapping the code that interacts with the external service with `run_with_circuit`, the method is executed within the circuit breaker.
+The circuit breaker is created and configured by the `circuit` method, which is called automatically when the `CircuitBreaker` module is included.
+The method should raise a custom error, that matches the `exceptions` from the concern.
+
+The circuit breaker tracks the number of errors and the rate of requests,
+and opens the circuit if it reaches the configured error threshold or volume threshold.
+If the circuit is open, subsequent requests fail fast without executing the code block, and the circuit breaker periodically allows a small number of requests through to test the service's availability before closing the circuit again.
+
+### Configuration
+
+The circuit breaker is configured with two constants which control the number of errors and requests at which the circuit will open:
+
+- `ERROR_THRESHOLD`
+- `VOLUME_THRESHOLD`
+
+You can adjust these values as needed for the specific service and usage pattern.
+The concern also raises an `InternalServerError` exception, which is counted towards the error threshold if raised during the execution of the code block.
+This is the exception class that triggers the circuit breaker when raised by the code that interacts with the external service.
+By default, the `CircuitBreaker` concern uses `StandardError`.
+
+NOTE:
+The service_name method must be implemented by the including class to provide a unique identifier for the service being protected. The `CircuitBreaker` module depends on the `Circuitbox` gem to provide the circuit breaker implementation.
+
+### Testing
+
+To test code that uses the `CircuitBreaker` concern, you can use `RSpec` shared examples and pass the `service` and `subject` variables:
+
+```ruby
+it_behaves_like 'has circuit breaker' do
+ let(:service) { dummy_class.new }
+ let(:subject) { service.dummy_method }
+end
+```
+
## How to implement a new action
### Register a new method
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index d8bdc56f265..40be7a7d094 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -193,6 +193,8 @@ This means it is skipped unless run with `--tag quarantine`:
bin/rspec --tag quarantine
```
+After the long-term quarantining MR has reached production, you should revert the fast-quarantine MR you created earlier.
+
### Jest
For Jest specs, you can use the `.skip` method along with the `eslint-disable-next-line` comment to disable the `jest/no-disabled-tests` ESLint rule and include the issue URL. Here's an example:
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 957790733c3..851cb156def 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -59,6 +59,22 @@ The Azure Storage Driver used to write to `//` as the default root directory. Th
In GitLab 16.0, the new default configuration for the storage driver uses `trimlegacyrootprefix: true`, and `/` is the default root directory. You can set your configuration to `trimlegacyrootprefix: false` if needed, to revert to the previous behavior.
+### Bundled Grafana Helm Chart
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+The Grafana Helm chart that was bundled with the GitLab Helm Chart is removed in the GitLab Helm Chart 7.0 release (releasing along with GitLab 16.0).
+
+The `global.grafana.enabled` setting for the GitLab Helm Chart has also been removed alongside the Grafana Helm chart.
+
+If you're using the bundled Grafana, you should switch to the [newer chart version from Grafana Labs](https://artifacthub.io/packages/helm/grafana/grafana)
+or a Grafana Operator from a trusted provider.
+
+In your new Grafana instance, you can [configure the GitLab provided Prometheus as a data source](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html#integration-with-gitlab-ui)
+and [connect Grafana to the GitLab UI](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html#integration-with-gitlab-ui).
+
### CAS OmniAuth provider is removed
WARNING:
@@ -86,6 +102,18 @@ Review the details carefully before upgrading.
The [GitLab Conan repository](https://docs.gitlab.com/ee/user/packages/conan_repository/) supports the `conan search` command, but when searching a project-level endpoint, instance-level Conan packages could have been returned. This unintended functionality is removed in GitLab 16.0. The search endpoint for the project level now only returns packages from the target project.
+### Configuring Redis config file paths using environment variables is no longer supported
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+You can no longer specify Redis configuration file locations
+using the environment variables like `GITLAB_REDIS_CACHE_CONFIG_FILE` or
+`GITLAB_REDIS_QUEUES_CONFIG_FILE`. Use the default
+configuration file locations instead, for example `config/redis.cache.yml` or
+`config/redis.queues.yml`.
+
### Container Registry pull-through cache is removed
WARNING:
@@ -94,6 +122,22 @@ Review the details carefully before upgrading.
The Container Registry [pull-through cache](https://docs.docker.com/registry/recipes/mirror/) was deprecated in GitLab 15.8 and removed in GitLab 16.0. This feature is part of the upstream [Docker Distribution project](https://github.com/distribution/distribution) but we are removing that code in favor of the GitLab Dependency Proxy. Use the GitLab Dependency Proxy to proxy and cache container images from Docker Hub.
+### Enforced validation of CI/CD parameter character lengths
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+Previously, only CI/CD [job names](https://docs.gitlab.com/ee/ci/jobs/index.html#job-name-limitations) had a strict 255-character limit. Now, more CI/CD keywords are validated to ensure they stay under the limit.
+
+The following to 255 characters are now strictly limited to 255 characters:
+
+- The `stage` keyword.
+- The `ref` parameter, which is the Git branch or tag name for the pipeline.
+- The `description` and `target_url` parameters, used by external CI/CD integrations.
+
+Users on self-managed instances should update their pipelines to ensure they do not use parameters that exceed 255 characters. Users on GitLab.com do not need to make any changes, as these parameters are already limited in that database.
+
### GitLab administrators must have permission to modify protected branches or tags
WARNING:
@@ -113,6 +157,44 @@ In GitLab 16.0 and later, the GraphQL query for runners will no longer return th
- `PAUSED` has been replaced with the field, `paused: true`.
- `ACTIVE` has been replaced with the field, `paused: false`.
+### Limit CI_JOB_TOKEN scope is disabled
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+In GitLab 14.4 we introduced the ability to [limit your project's CI/CD job token](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#limit-your-projects-job-token-access) (`CI_JOB_TOKEN`) access to make it more secure. You could use the **Limit CI_JOB_TOKEN access** setting to prevent job tokens from your project's pipelines from being used to **access other projects**. When enabled with no other configuration, your pipelines could not access any other projects. To use job tokens to access other projects from your project's pipelines, you needed to list those other projects explicitly in the setting's allowlist, and you needed to be a maintainer in _all_ the projects. You might have seen this mentioned as the "outbound scope" of the job token.
+
+The job token functionality was updated in 15.9 with a [better security setting](https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html#allow-access-to-your-project-with-a-job-token). Instead of securing your own project's job tokens from accessing other projects, the new workflow is to secure your own project from being accessed by other projects' job tokens without authorization. You can see this as an "inbound scope" for job tokens. When this new **Allow access to this project with a CI_JOB_TOKEN** setting is enabled with no other configuration, job tokens from other projects cannot **access your project**. If you want a project to have access to your own project, you must list it in the new setting's allowlist. You must be a maintainer in your own project to control the new allowlist, but you only need to have the Guest role in the other projects. This new setting is enabled by default for all new projects.
+
+In GitLab 16.0, the old **Limit CI_JOB_TOKEN access** setting is disabled by default for all **new** projects. In existing projects with this setting currently enabled, it will continue to function as expected, but you are unable to add any more projects to the old allowlist. If the setting is disabled in any project, it is not possible to re-enable this setting in 16.0 or later. To control access between your projects, use the new **Allow access** setting instead.
+
+In 17.0, we plan to remove the **Limit** setting completely, and set the **Allow access** setting to enabled for all projects. This change ensures a higher level of security between projects. If you currently use the **Limit** setting, you should update your projects to use the **Allow access** setting instead. If other projects access your project with a job token, you must add them to the **Allow access** setting's allowlist.
+
+To prepare for this change, users on GitLab.com or self-managed GitLab 15.9 or later can enable the **Allow access** setting now and add the other projects. It will not be possible to disable the setting in 17.0 or later.
+
+### Maximum number of active pipelines per project limit (`ci_active_pipelines`)
+
+The [**Maximum number of active pipelines per project** limit](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#set-cicd-limits) has been removed. Instead, use the other recommended rate limits that offer similar protection:
+
+- [**Pipelines rate limits**](https://docs.gitlab.com/ee/user/admin_area/settings/rate_limit_on_pipelines_creation.html).
+- [**Total number of jobs in currently active pipelines**](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#set-cicd-limits).
+
+### Non-standard default Redis ports are no longer supported
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+If GitLab starts without any Redis configuration file present,
+GitLab assumes it can connect to three Redis servers at `localhost:6380`,
+`localhost:6381` and `localhost:6382`. We are changing this behavior
+so GitLab assumes there is one Redis server at `localhost:6379`.
+
+If you want to keep using the three servers, you must configure
+the Redis URLs by editing the `config/redis.cache.yml`,`config/redis.queues.yml`,
+and `config/redis.shared_state.yml` files.
+
### PipelineSecurityReportFinding name GraphQL field
WARNING:
@@ -235,6 +317,26 @@ From GitLab 15.9, all Release links are external. The `external` field in the Re
As of GitLab 16.0, GitLab Runner images based on Windows Server 2004 and 20H2 will not be provided as these operating systems are end-of-life.
+### The stable Terraform CI/CD template has been replaced with the latest template
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+With every major GitLab version, we update the stable Terraform templates with the current latest templates.
+This change affects the [quickstart](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml)
+and the [base](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml) templates.
+
+The new templates do not change the directory to `$TF_ROOT` explicitly: `gitlab-terraform` gracefully
+handles directory changing. If you altered the job scripts to assume that the current working directory is `$TF_ROOT`, you must manually add `cd "$TF_ROOT"` now.
+
+Because the latest template introduces Merge Request Pipeline support which is not supported in Auto DevOps,
+those rules are not yet integrated into the stable template.
+However, we may introduce them later on, which may break your Terraform pipelines in regards to which jobs are executed.
+
+To accommodate the changes, you might need to adjust the [`rules`](https://docs.gitlab.com/ee/ci/yaml/#rules) in your
+`.gitlab-ci.yml` file.
+
### Two DAST API variables have been removed
WARNING:
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 501c9b7f93b..64bec01b765 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -54,6 +54,7 @@ tier. Users can continue to access the features in a paid tier without sharing u
- [Maintenance mode](../../../administration/maintenance_mode/index.md).
- [Configurable issue boards](../../project/issue_board.md#configurable-issue-boards).
+- [Coverage-guided fuzz testing](../../application_security/coverage_fuzzing/index.md).
NOTE:
Registration is not yet required for participation, but may be added in a future milestone.
diff --git a/doc/user/project/git_attributes.md b/doc/user/project/git_attributes.md
index 1feb17b19c8..034fc1dc079 100644
--- a/doc/user/project/git_attributes.md
+++ b/doc/user/project/git_attributes.md
@@ -20,6 +20,33 @@ The `.gitattributes` file _must_ be encoded in UTF-8 and _must not_ contain a
Byte Order Mark. If a different encoding is used, the file's contents are
ignored.
+## Support for Mixed File Encodings
+
+GitLab attempts to detect the encoding of files automatically, but defaults to UTF-8 unless
+the detector is confident of a different type (such as `ISO-8859-1`). Incorrect encoding
+detection can result in some characters not displaying in the text, such as accented characters in a
+non-UTF-8 encoding.
+
+Git has built-in support for handling this eventuality and automatically converts files between
+a designated encoding and UTF-8 for the repository itself. Configure support for mixed file encoding in the `.gitattributes`
+file using the `working-tree-encoding` attribute.
+
+Example:
+
+```plaintext
+*.xhtml text working-tree-encoding=ISO-8859-1
+```
+
+With this example configuration, Git maintains all `.xhtml` files in the repository in ISO-8859-1
+encoding in the local tree, but converts to and from UTF-8 when committing into the repository. GitLab
+renders the files accurately as it only sees correctly encoded UTF-8.
+
+If applying this configuration to an existing repository, files may need to be touched and recommitted
+if the local copy has the correct encoding but the repository does not. This can
+be performed for the whole repository by running `git add --renormalize .`.
+
+For more information, see [working-tree-encoding](https://git-scm.com/docs/gitattributes#_working_tree_encoding).
+
## Syntax Highlighting
The `.gitattributes` file can be used to define which language to use when
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 5324606c1b8..3273e1c0be8 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -129,6 +129,8 @@ threads. Some quick actions might not be available to all subscription tiers.
| `/unsubscribe` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Unsubscribe from notifications.
| `/weight <value>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set weight. Valid options for `<value>` include `0`, `1`, `2`, and so on.
| `/zoom <Zoom URL>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add a Zoom meeting to this issue or incident. In [GitLab 15.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/230853) users on GitLab Premium can add a short description when [adding a Zoom link to an incident](../../operations/incident_management/linked_resources.md#link-zoom-meetings-from-an-incident).
+| `/blocks <issue1> <issue2>` | **{check-circle}** Yes | **{check-circle}** No | **{dotted-circle}** No | Mark the issue as blocking other issues. The `<issue>` value should be in the format of `#issue`, `group/project#issue`, or the full issue URL. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214232) in GitLab 16.0).
+| `/blocked_by <issue1> <issue2>` | **{check-circle}** Yes | **{check-circle}** No | **{dotted-circle}** No | Mark the issue as blocked by other issues. The `<issue>` value should be in the format of `#issue`, `group/project#issue`, or the full issue URL. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214232) in GitLab 16.0).
## Work items
diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md
index 0ec351edfff..60688ac9356 100644
--- a/doc/user/workspace/index.md
+++ b/doc/user/workspace/index.md
@@ -16,40 +16,43 @@ This feature is in [Beta](../../policy/alpha-beta-support.md#beta) and subject t
A workspace is a virtual sandbox environment for your code in GitLab. You can use workspaces to create and manage isolated development environments for your GitLab projects. These environments ensure that different projects don't interfere with each other.
-You can create a workspace on its own or as part of a project. Each workspace includes its own set of dependencies, libraries, and tools, which you can customize to meet the specific needs of each project.
+Each workspace includes its own set of dependencies, libraries, and tools, which you can customize to meet the specific needs of each project. Workspaces use the AMD64 architecture.
-## Run a workspace
-
-To run a workspace:
+## Create a workspace
-1. Set up a Kubernetes cluster that supports the GitLab agent for Kubernetes. See the [supported Kubernetes versions](../clusters/agent/index.md#gitlab-agent-for-kubernetes-supported-cluster-versions).
-1. Ensure autoscaling for Kubernetes cluster is enabled.
-1. In the Kubernetes cluster, verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/) is defined so that volumes can be dynamically provisioned for each workspace.
-1. [Install the GitLab agent for Kubernetes](../clusters/agent/install/index.md).
-1. Configure remote development settings for the GitLab agent with the provided snippet.
-1. Install an Ingress controller of your choice (for example, `ingress-nginx`), and make it accessible over a domain.
-1. [Install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions).
-1. In each public project you want to use this feature for, define a [devfile](#devfile). Ensure the container images used in the devfile support [arbitrary user IDs](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
+Prerequisites:
-## Configure the GitLab agent for Kubernetes
+- Set up a Kubernetes cluster that the GitLab agent for Kubernetes supports. See the [supported Kubernetes versions](../clusters/agent/index.md#gitlab-agent-for-kubernetes-supported-cluster-versions).
+- Ensure autoscaling for the Kubernetes cluster is enabled.
+- In the Kubernetes cluster, verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/) is defined so that volumes can be dynamically provisioned for each workspace.
+- In the Kubernetes cluster, install an Ingress controller of your choice (for example, `ingress-nginx`), and make that controller accessible over a domain. For example, point `*.workspaces.example.dev` and `workspaces.example.dev` to the load balancer exposed by the Ingress controller.
+- In the Kubernetes cluster, [install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions).
+- In the Kubernetes cluster, [install the GitLab agent for Kubernetes](../clusters/agent/install/index.md).
+- Configure remote development settings for the GitLab agent with this snippet:
-To provision and communicate with the workspace, the GitLab agent for Kubernetes must be running on your cluster. To configure the GitLab agent for Kubernetes:
+ ```yaml
+ remote_development:
+ enabled: true
+ dns_zone: "workspaces.example.dev"
+ ```
-1. [Install GitLab Runner on the machine where you want to configure the agent](https://docs.gitlab.com/runner/install/).
-1. Deploy the GitLab agent with the provided [YAML manifests](https://gitlab.com/gitlab-examples/ops/gitops-demo/k8s-agents/-/tree/main/manifests). The system does not impose any restrictions on the manner in which pods interact with each other. See [Pod interaction in a cluster](#pod-interaction-in-a-cluster).
-1. Customize the GitLab agent configuration by editing the agent `ConfigMap`. `ConfigMap` is used to configure settings such as the GitLab URL and the registration token. For more information about the available configuration options, see [Connecting a Kubernetes cluster with GitLab](../clusters/agent/index.md).
-1. Deploy the updated `ConfigMap` by running this command:
+ Update `dns_zone` as needed.
- ```plaintext
- kubectl apply -f <path-to-configmap.yaml>
- ```
+- In each public project you want to use this feature for, define a [devfile](#devfile). Ensure the container images used in the devfile support [arbitrary user IDs](#arbitrary-user-ids).
-1. Configure the agent to run on specific Kubernetes nodes by using labels:
+To create a workspace in GitLab:
- 1. To assign labels to nodes, use the `kubectl label` command.
- 1. To configure the agent to only run on nodes with a specific label, use the `nodeSelector` field in the GitLab agent deployment YAML.
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. In the root directory of your project, create a file named `.devfile.yaml`.
+1. On the left sidebar, select **Workspaces**.
+1. In the upper right, select **New workspace**.
+1. From the **Select project** dropdown list, select a project with a `.devfile.yaml` file. You can only create workspaces for public projects.
+1. From the **Select cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to.
+1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates. This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely.
+1. Select **Create workspace**.
-You can remove an agent by using the GitLab UI or the GraphQL API. The agent and any associated tokens are removed from GitLab, but no changes are made in your Kubernetes cluster. You must clean up those resources manually. See [Remove an agent](../clusters/agent/work_with_agent.md#remove-an-agent).
+The workspace might take a few minutes to start. To access the workspace, under **Preview**, select the workspace link.
+You also have access to the terminal and can install any necessary dependencies.
## Devfile
@@ -61,22 +64,22 @@ This way, you can create consistent and reproducible development environments re
### Relevant schema properties
-GitLab only supports the `container` component in [devfile 2.2.0](https://devfile.io/docs/2.2.0/devfile-schema).
-Use this component to define a container image as the execution environment for a devfile workspace.
+GitLab only supports the `container` and `volume` components in [devfile 2.2.0](https://devfile.io/docs/2.2.0/devfile-schema).
+Use the `container` component to define a container image as the execution environment for a devfile workspace.
You can specify the base image, dependencies, and other settings.
-Only these properties are relevant to the GitLab implementation of devfile:
+Only these properties are relevant to the GitLab implementation of the `container` component:
| Properties | Definition |
|----------------| ----------------------------------------------------------------------------------|
| `image` | Name of the container image to use for the workspace. |
+| `memoryRequest`| Minimum amount of memory the container can use. |
| `memoryLimit` | Maximum amount of memory the container can use. |
+| `cpuRequest` | Minimum amount of CPU the container can use. |
| `cpuLimit` | Maximum amount of CPU the container can use. |
-| `mountSources` | Whether to mount the source code directory from the workspace into the container. |
-| `workingDir` | Working directory to use in the container. |
-| `commands` | Commands to run in the container. |
-| `args` | Arguments to pass to the commands. |
-| `ports` | Port mappings to expose from the container. |
+| `env` | Environment variables to use in the container. |
+| `endpoints` | Port mappings to expose from the container. |
+| `volumeMounts` | Storage volume to mount in the container. |
### Example definition
@@ -90,32 +93,18 @@ components:
gl/inject-editor: true
container:
image: registry.gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/debian-bullseye-ruby-3.2-node-18.12:rubygems-3.4-git-2.33-lfs-2.9-yarn-1.22-graphicsmagick-1.3.36-gitlab-workspaces
+ env:
+ - name: KEY
+ value: VALUE
endpoints:
- name: http-3000
targetPort: 3000
```
-For other syntax examples, see the [`demos` projects](https://gitlab.com/gitlab-org/remote-development/demos).
-
-## Create a workspace
-
-Prerequisite:
-
-- You must have [configured the GitLab agent for Kubernetes](#configure-the-gitlab-agent-for-kubernetes).
-
-To create a workspace in GitLab:
-
-1. On the top bar, select **Main menu > Projects** and find your project.
-1. In the root directory of your project, create a file named `.devfile.yaml`.
-1. On the left sidebar, select **Workspaces**.
-1. In the upper right, select **New workspace**.
-1. From the **Select project** dropdown list, select a project with a `.devfile.yaml` file. You can only create workspaces for public projects.
-1. From the **Select cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to.
-1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates. This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely.
-1. Select **Create workspace**.
+For more information, see the [devfile documentation](https://devfile.io/docs/2.2.0/devfile-schema).
+For other examples, see the [`examples` projects](https://gitlab.com/gitlab-org/remote-development/examples).
-The workspace might take a few minutes to start. When the workspace is ready, use the [Web IDE](../project/web_ide/index.md) to access your development environment.
-You also have access to the terminal and can install any necessary dependencies.
+This container image is for demonstration purposes only. To use your own container image, see [Arbitrary user IDs](#arbitrary-user-ids).
## Web IDE
@@ -125,71 +114,42 @@ The Web IDE is powered by the [GitLab VS Code fork](https://gitlab.com/gitlab-or
## Private repositories
-You cannot create a workspace for a private repository because you cannot verify your identity. You can only clone or access public repositories.
-
-You can clone a public repository over:
+You cannot create a workspace for a private repository because GitLab does not inject any credentials into the workspace. You can only create a workspace for public repositories that have a devfile.
-- **HTTPS**: You must provide a personal access token every time you access a public repository or create a workspace. This token acts as a password and grants access to a specific resource.
-- **SSH**: You don't have to enter your password or personal access token when you access a public repository. However, you must provide your SSH key or personal access token every time you create a workspace.
+From a workspace, you can clone any repository manually.
## Pod interaction in a cluster
-The system does not impose any restrictions on the manner in which pods interact with each other. It's the client's responsibility to restrict network access to the Kubernetes control plane as GitLab cannot determine the location of the API.
+Workspaces run as pods in a Kubernetes cluster. GitLab does not impose any restrictions on the manner in which pods interact with each other.
Because of this requirement, you might want to isolate this feature from other containers in your cluster.
-## Networking and security
-
-Workspaces are isolated environments that are only provisioned when you start a new instance. These environments are isolated from the host machine.
-
-Workspaces use virtual network interfaces to connect to the internet and other resources, which helps prevent conflicts with the host machine's network settings.
+## Network access and workspace authorization
-### SSL, TLS, and HTTPS
+It's the client's responsibility to restrict network access to the Kubernetes control plane as GitLab does not have control over the API.
-Workspaces use SSL and TLS to provide secure and isolated development environments that you can access from anywhere.
+Only the workspace creator can access the workspace and any endpoints exposed in that workspace. The workspace creator is only authorized to access the workspace after user authentication with OAuth.
-Workspaces support HTTPS, which uses Transport Layer Security (TLS) to encrypt data sent between your machine and the workspace. Workspaces generate and manage their own SSL certificates for HTTPS connections. These SSL certificates are automatically renewed.
+## Compute resources and volume storage
-Workspaces also support Let's Encrypt SSL certificates, which you can use to enable HTTPS connections with a custom domain name.
+When you stop a workspace, the compute resources for that workspace are scaled down to zero. However, the volume provisioned for the workspace still exists.
-### Workspace authorization
+To delete the provisioned volume, you must terminate the workspace.
-To use workspaces, you must have a GitLab account with the necessary permissions to create or access a repository. GitLab authentication is used to control access to workspaces. Only users who have been granted access to a repository can create or access workspaces associated with that repository.
+## Disable remote development in the GitLab agent for Kubernetes
-GitLab also provides administrators with the ability to:
+You can stop the `remote_development` module of the GitLab agent for Kubernetes from communicating with GitLab. To disable remote development in the GitLab agent configuration, set this property:
-- Limit who can create workspaces.
-- Set resource limits for workspaces.
-- Configure the default environment for workspaces.
-
-## Workspace lifecycle
-
-The lifecycle of a workspace is divided into the following stages:
-
-- **Creation**: A workspace is created when you open a new workspace session from a GitLab repository. GitLab creates a virtual machine instance in the cloud with the necessary software and tools for your specific project.
-- **Initialization**: The instance is initialized with the project files and dependencies when you clone the repository or pull from a container registry.
-- **Usage**: The workspace is ready to use. You can use the IDE and command-line tools that come with the workspace or install any other tools.
-- **Persistence**: Any changes made to the project files and dependencies in the workspace persist to the GitLab repository in real-time. This way, these changes can be synced and shared with other collaborators.
-- **Deletion**: When you're finished with the workspace session, you can suspend or delete the workspace. Suspending the workspace pauses billing but keeps the instance running. Deleting the workspace removes the instance and all associated data permanently.
-
-## Container best practices
-
-### Set a user to run a container in Kubernetes
-
-GitLab cannot predict which user is the best fit to run a container in Kubernetes. You must set the user yourself to ensure the container runs correctly.
-
-To set a user to run a container in Kubernetes, follow these best practices:
-
-- When you create a [devfile](#devfile) for the container, ensure the container images used in the devfile support [arbitrary user IDs](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
-- For each container in your project, you must explicitly set the Linux user ID to a random value. The default value for GitLab is `5001`.
-- You must set the fields to prevent any privilege escalation for the Linux user.
-
-CRI-O, the container runtime interface used by OpenShift, has a default group ID of `0` for all containers. If the container images support arbitrary user IDs, all files become editable as a Linux root group member. To solve this issue, GitLab sets arbitrary user IDs for all containers.
+```yaml
+remote_development:
+ enabled: false
+```
-### Architectural support
+If you already have running workspaces, an administrator must manually delete these workspaces in Kubernetes.
-Workspaces use the AMD64 architecture because modern software is generally compatible with this architecture. If you're using other architectures (such as ARM), you can cross-compile your code to run on AMD64 systems.
+## Arbitrary user IDs
-### Namespace deletion
+You can provide your own container image, which can run as any Linux user ID. It's not possible for GitLab to predict the Linux user ID for a container image.
+GitLab uses the Linux root group ID permission to create, update, or delete files in the container. CRI-O, the container runtime interface used by Kubernetes, has a default group ID of `0` for all containers.
-To delete a namespace, Kubernetes administrators must manually delete the namespace. If you're running a workspace on your own environment, it's your responsibility to manage and delete namespaces.
+If you have a container image that does not support arbitrary user IDs, you cannot create, update, or delete files in a workspace. To create a container image that supports arbitrary user IDs, see the [OpenShift documentation](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index 3db98ef2838..dee04b6bb00 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -8,7 +8,7 @@ module API
def can_lint_ci?
signup_unrestricted = Gitlab::CurrentSettings.signup_enabled? && !Gitlab::CurrentSettings.signup_limited?
internal_user = current_user.present? && !current_user.external?
- is_developer = current_user.present? && current_user.projects.any? { |p| p.team.member?(current_user, Gitlab::Access::DEVELOPER) }
+ is_developer = current_user.present? && current_user.projects.any? { |p| p.member?(current_user, Gitlab::Access::DEVELOPER) }
signup_unrestricted || internal_user || is_developer
end
diff --git a/lib/banzai/filter/inline_embeds_filter.rb b/lib/banzai/filter/inline_embeds_filter.rb
index c1077674cf0..a16166123f8 100644
--- a/lib/banzai/filter/inline_embeds_filter.rb
+++ b/lib/banzai/filter/inline_embeds_filter.rb
@@ -10,6 +10,8 @@ module Banzai
# the link, and insert this node after any html content
# surrounding the link.
def call
+ return doc if Feature.enabled?(:remove_monitor_metrics)
+
doc.xpath(xpath_search).each do |node|
next unless element = element_to_embed(node)
diff --git a/lib/banzai/filter/inline_observability_filter.rb b/lib/banzai/filter/inline_observability_filter.rb
index a47b57d94dd..8e38f689959 100644
--- a/lib/banzai/filter/inline_observability_filter.rb
+++ b/lib/banzai/filter/inline_observability_filter.rb
@@ -2,13 +2,21 @@
module Banzai
module Filter
- class InlineObservabilityFilter < ::Banzai::Filter::InlineEmbedsFilter
+ class InlineObservabilityFilter < HTML::Pipeline::Filter
include Gitlab::Utils::StrongMemoize
def call
return doc unless Gitlab::Observability.enabled?(group)
- super
+ doc.xpath(xpath_search).each do |node|
+ next unless element = element_to_embed(node)
+
+ # We want this to follow any surrounding content. For example,
+ # if a link is inline in a paragraph.
+ node.parent.children.last.add_next_sibling(element)
+ end
+
+ doc
end
# Placeholder element for the frontend to use as an
@@ -46,6 +54,10 @@ module Banzai
def group
context[:group] || context[:project]&.group
end
+
+ def gitlab_domain
+ ::Gitlab.config.gitlab.url
+ end
end
end
end
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index 1709b607c2e..5983036a8e5 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -139,11 +139,7 @@ module Banzai
end
def team_member?(user)
- if parent_group?
- parent.member?(user)
- else
- parent.team.member?(user)
- end
+ parent.member?(user)
end
def parent_url(link_content, author)
diff --git a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
index 51bcbd278d5..2661c208665 100644
--- a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
@@ -24,6 +24,9 @@ validate:
build:
extends: .terraform:build
+ environment:
+ name: $TF_STATE_NAME
+ action: prepare
deploy:
extends: .terraform:deploy
@@ -31,3 +34,4 @@ deploy:
- build
environment:
name: $TF_STATE_NAME
+ action: start
diff --git a/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
index dd1676f25b6..cfade84a533 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.gitlab-ci.yml
@@ -9,7 +9,7 @@
# There is a more opinionated template which we suggest the users to abide,
# which is the lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
image:
- name: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/terraform-images/releases/1.1:v0.43.0"
+ name: "$CI_TEMPLATE_REGISTRY_HOST/gitlab-org/terraform-images/releases/1.4:v1.0.0"
variables:
TF_ROOT: ${CI_PROJECT_DIR} # The relative path to the root directory of the Terraform project
@@ -23,24 +23,24 @@ cache:
.terraform:fmt: &terraform_fmt
stage: validate
script:
- - cd "${TF_ROOT}"
- gitlab-terraform fmt
allow_failure: true
.terraform:validate: &terraform_validate
stage: validate
script:
- - cd "${TF_ROOT}"
- gitlab-terraform validate
.terraform:build: &terraform_build
stage: build
script:
- - cd "${TF_ROOT}"
- gitlab-terraform plan
- gitlab-terraform plan-json
resource_group: ${TF_STATE_NAME}
artifacts:
+ # The next line, which disables public access to pipeline artifacts, may not be available everywhere.
+ # See: https://docs.gitlab.com/ee/ci/yaml/#artifactspublic
+ public: false
paths:
- ${TF_ROOT}/plan.cache
reports:
@@ -49,7 +49,6 @@ cache:
.terraform:deploy: &terraform_deploy
stage: deploy
script:
- - cd "${TF_ROOT}"
- gitlab-terraform apply
resource_group: ${TF_STATE_NAME}
rules:
@@ -59,7 +58,6 @@ cache:
.terraform:destroy: &terraform_destroy
stage: cleanup
script:
- - cd "${TF_ROOT}"
- gitlab-terraform destroy
resource_group: ${TF_STATE_NAME}
when: manual
diff --git a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
index 3249bd2bcac..88fe55a44ab 100644
--- a/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform/Base.latest.gitlab-ci.yml
@@ -24,7 +24,6 @@ cache:
.terraform:fmt: &terraform_fmt
stage: validate
script:
- - cd "${TF_ROOT}"
- gitlab-terraform fmt
allow_failure: true
rules:
@@ -36,7 +35,6 @@ cache:
.terraform:validate: &terraform_validate
stage: validate
script:
- - cd "${TF_ROOT}"
- gitlab-terraform validate
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
@@ -47,7 +45,6 @@ cache:
.terraform:build: &terraform_build
stage: build
script:
- - cd "${TF_ROOT}"
- gitlab-terraform plan
- gitlab-terraform plan-json
resource_group: ${TF_STATE_NAME}
@@ -68,7 +65,6 @@ cache:
.terraform:deploy: &terraform_deploy
stage: deploy
script:
- - cd "${TF_ROOT}"
- gitlab-terraform apply
resource_group: ${TF_STATE_NAME}
rules:
@@ -79,7 +75,6 @@ cache:
.terraform:destroy: &terraform_destroy
stage: cleanup
script:
- - cd "${TF_ROOT}"
- gitlab-terraform destroy
resource_group: ${TF_STATE_NAME}
when: manual
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
index fdbf068303f..2276f2fc3c6 100644
--- a/lib/gitlab/cycle_analytics/stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -45,7 +45,7 @@ module Gitlab
end
def user_has_sufficient_access?
- @project.team.member?(@current_user, Gitlab::Access::REPORTER)
+ @project.member?(@current_user, Gitlab::Access::REPORTER)
end
def serialize(summary_object, with_unit: false)
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index ef8cb38029a..904a2ccc79b 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -67,7 +67,7 @@ module Gitlab
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:vscode_web_ide, current_user)
push_frontend_feature_flag(:super_sidebar_peek, current_user)
- push_frontend_feature_flag(:unbatch_graphql_queries)
+ push_frontend_feature_flag(:unbatch_graphql_queries, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
end
diff --git a/lib/gitlab/quick_actions/relate_actions.rb b/lib/gitlab/quick_actions/relate_actions.rb
index 4c8035f192e..b8cbfdefda1 100644
--- a/lib/gitlab/quick_actions/relate_actions.rb
+++ b/lib/gitlab/quick_actions/relate_actions.rb
@@ -8,19 +8,27 @@ module Gitlab
included do
desc { _('Mark this issue as related to another issue') }
- explanation do |related_reference|
- _('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
+ explanation do |target_issues|
+ _('Marks this issue as related to %{issue_ref}.') % { issue_ref: target_issues.to_sentence }
end
- execution_message do |related_reference|
- _('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
+ execution_message do |target_issues|
+ _('Marked this issue as related to %{issue_ref}.') % { issue_ref: target_issues.to_sentence }
end
- params '#issue'
+ params '<#issue | group/project#issue | issue URL>'
types Issue
- condition do
- current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
+ condition { can_relate_issues? }
+ parse_params { |issues| format_params(issues) }
+ command :relate do |target_issues|
+ create_links(target_issues)
end
- command :relate do |related_reference|
- service = IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_reference] })
+
+ private
+
+ def create_links(references, type: 'relates_to')
+ service = IssueLinks::CreateService.new(
+ quick_action_target,
+ current_user, { issuable_references: references, link_type: type }
+ )
create_issue_link = proc { service.execute }
if quick_action_target.persisted?
@@ -29,6 +37,14 @@ module Gitlab
quick_action_target.run_after_commit(&create_issue_link)
end
end
+
+ def can_relate_issues?
+ current_user.can?(:admin_issue_link, quick_action_target)
+ end
+
+ def format_params(issue_references)
+ issue_references.split(' ')
+ end
end
end
end
diff --git a/lib/gitlab/usage_data_counters/known_events/quickactions.yml b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
index 2521331fee3..136d284f462 100644
--- a/lib/gitlab/usage_data_counters/known_events/quickactions.yml
+++ b/lib/gitlab/usage_data_counters/known_events/quickactions.yml
@@ -129,3 +129,7 @@
aggregation: weekly
- name: i_quickactions_type
aggregation: weekly
+- name: i_quickactions_blocked_by
+ aggregation: weekly
+- name: i_quickactions_blocks
+ aggregation: weekly
diff --git a/lib/sidebars/groups/menus/issues_menu.rb b/lib/sidebars/groups/menus/issues_menu.rb
index 27b78d89aa1..ddc930d56ff 100644
--- a/lib/sidebars/groups/menus/issues_menu.rb
+++ b/lib/sidebars/groups/menus/issues_menu.rb
@@ -101,7 +101,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Milestones'),
link: group_milestones_path(context.group),
- super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::ManageMenu,
+ super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::PlanMenu,
active_routes: { path: 'milestones#index' },
item_id: :milestones
)
diff --git a/lib/sidebars/groups/super_sidebar_menus/manage_menu.rb b/lib/sidebars/groups/super_sidebar_menus/manage_menu.rb
index 71214c42bf5..f926892083b 100644
--- a/lib/sidebars/groups/super_sidebar_menus/manage_menu.rb
+++ b/lib/sidebars/groups/super_sidebar_menus/manage_menu.rb
@@ -19,9 +19,7 @@ module Sidebars
[
:activity,
:members,
- :labels,
- :milestones,
- :iterations
+ :labels
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
end
end
diff --git a/lib/sidebars/groups/super_sidebar_menus/plan_menu.rb b/lib/sidebars/groups/super_sidebar_menus/plan_menu.rb
index 74e7977fff5..bf122f930a2 100644
--- a/lib/sidebars/groups/super_sidebar_menus/plan_menu.rb
+++ b/lib/sidebars/groups/super_sidebar_menus/plan_menu.rb
@@ -22,6 +22,8 @@ module Sidebars
:issue_boards,
:epic_boards,
:roadmap,
+ :milestones,
+ :iterations,
:group_wiki,
:crm_contacts,
:crm_organizations
diff --git a/lib/sidebars/projects/menus/issues_menu.rb b/lib/sidebars/projects/menus/issues_menu.rb
index 070eac0ae49..dc40b84529f 100644
--- a/lib/sidebars/projects/menus/issues_menu.rb
+++ b/lib/sidebars/projects/menus/issues_menu.rb
@@ -121,7 +121,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Service Desk'),
link: service_desk_project_issues_path(context.project),
- super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::PlanMenu,
+ super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::MonitorMenu,
active_routes: { path: 'issues#service_desk' },
item_id: :service_desk
)
@@ -131,7 +131,7 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Milestones'),
link: project_milestones_path(context.project),
- super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::ManageMenu,
+ super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::PlanMenu,
active_routes: { controller: :milestones },
item_id: :milestones
)
diff --git a/lib/sidebars/projects/super_sidebar_menus/manage_menu.rb b/lib/sidebars/projects/super_sidebar_menus/manage_menu.rb
index faf9708604d..f36588628a9 100644
--- a/lib/sidebars/projects/super_sidebar_menus/manage_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/manage_menu.rb
@@ -19,9 +19,7 @@ module Sidebars
[
:activity,
:members,
- :labels,
- :milestones,
- :iterations
+ :labels
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
end
end
diff --git a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
index af621a0f46f..fb56f6f3792 100644
--- a/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/monitor_menu.rb
@@ -22,7 +22,8 @@ module Sidebars
:alert_management,
:incidents,
:on_call_schedules,
- :escalation_policies
+ :escalation_policies,
+ :service_desk
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
end
end
diff --git a/lib/sidebars/projects/super_sidebar_menus/plan_menu.rb b/lib/sidebars/projects/super_sidebar_menus/plan_menu.rb
index bc3111cc6f4..dc35b37fe70 100644
--- a/lib/sidebars/projects/super_sidebar_menus/plan_menu.rb
+++ b/lib/sidebars/projects/super_sidebar_menus/plan_menu.rb
@@ -19,8 +19,9 @@ module Sidebars
[
:project_issue_list,
:boards,
+ :milestones,
+ :iterations,
:project_wiki,
- :service_desk,
:requirements
].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) }
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a6cd2389a3d..669e97cf0e7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11974,7 +11974,7 @@ msgstr ""
msgid "ContainerRegistry|Tags successfully marked for deletion."
msgstr ""
-msgid "ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept."
+msgid "ContainerRegistry|Tags that match %{strongStart}any of%{strongEnd} these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{strongStart}latest%{strongEnd} tag is always kept."
msgstr ""
msgid "ContainerRegistry|Tags that match these rules are %{strongStart}removed%{strongEnd}, unless a rule above says to keep them."
@@ -21236,6 +21236,12 @@ msgstr ""
msgid "GroupSelect|Select a group"
msgstr ""
+msgid "GroupSettings|After the instance reaches the user cap, any user who is added or requests access must be approved by an administrator. Leave empty for an unlimited user cap. If you change the user cap to unlimited, you must re-enable %{project_sharing_docs_link_start}project sharing%{link_end} and %{group_sharing_docs_link_start}group sharing%{link_end}."
+msgstr ""
+
+msgid "GroupSettings|After the instance reaches the user cap, any user who is added or requests access must be approved by an administrator. Leave empty for an unlimited user cap. If you change the user cap to unlimited, you must re-enable %{project_sharing_docs_link_start}project sharing%{link_end} and %{group_sharing_docs_link_start}group sharing%{link_end}. Increasing the user cap does not automatically approve pending users."
+msgstr ""
+
msgid "GroupSettings|Analytics"
msgstr ""
@@ -21401,12 +21407,6 @@ msgstr ""
msgid "GroupSettings|What is Insights?"
msgstr ""
-msgid "GroupSettings|When the number of active users exceeds this number, additional users must be %{user_cap_docs_link_start}approved by an owner%{user_cap_docs_link_end}. Leave empty if you don't want to enforce approvals."
-msgstr ""
-
-msgid "GroupSettings|When the number of active users exceeds this number, additional users must be %{user_cap_docs_link_start}approved by an owner%{user_cap_docs_link_end}. Leave empty if you don't want to enforce approvals. Increasing the user cap will not automatically approve pending users."
-msgstr ""
-
msgid "GroupSettings|You must have the Owner role in the target group"
msgstr ""
@@ -24590,6 +24590,9 @@ msgstr ""
msgid "IssuableEvents|requested review from"
msgstr ""
+msgid "IssuableEvents|resolved all threads"
+msgstr ""
+
msgid "IssuableEvents|unassigned"
msgstr ""
@@ -27128,6 +27131,9 @@ msgstr ""
msgid "Mark this issue as a duplicate of another issue"
msgstr ""
+msgid "Mark this issue as blocked by other issues"
+msgstr ""
+
msgid "Mark this issue as related to another issue"
msgstr ""
@@ -27191,6 +27197,9 @@ msgstr ""
msgid "Marked"
msgstr ""
+msgid "Marked %{target} as blocked by this issue."
+msgstr ""
+
msgid "Marked For Deletion At - %{deletion_time}"
msgstr ""
@@ -27203,6 +27212,9 @@ msgstr ""
msgid "Marked this %{noun} as ready."
msgstr ""
+msgid "Marked this issue as blocked by %{target}."
+msgstr ""
+
msgid "Marked this issue as related to %{issue_ref}."
msgstr ""
@@ -27572,9 +27584,6 @@ msgstr ""
msgid "MemberRole|maximum number of Member Roles are already in use by the group hierarchy. Please delete an existing Member Role."
msgstr ""
-msgid "MemberRole|minimal base access level must be %{min_access_level}."
-msgstr ""
-
msgid "MemberRole|must be top-level namespace"
msgstr ""
@@ -41561,6 +41570,12 @@ msgstr ""
msgid "Set the per-user rate limit for notes created by web or API requests."
msgstr ""
+msgid "Set this issue as blocked by %{target}."
+msgstr ""
+
+msgid "Set this issue as blocking %{target}."
+msgstr ""
+
msgid "Set this number to 0 to disable the limit."
msgstr ""
@@ -42933,6 +42948,9 @@ msgstr ""
msgid "Specified URL cannot be used: \"%{reason}\""
msgstr ""
+msgid "Specifies that this issue blocks other issues"
+msgstr ""
+
msgid "Specify IP ranges that are always allowed for inbound traffic, for use with group-level IP restrictions. Runner and Pages daemon internal IPs should be listed here so that they can access project artifacts."
msgstr ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 2671b9e6b8b..d7dec5f17d5 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -3,6 +3,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 10', '>= 10.3.0', require: 'gitlab/qa'
+gem 'gitlab_quality-test_tooling', '~> 0.2.2', require: false
gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.20.0'
gem 'capybara', '~> 3.39.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 278f902d8d3..c8d408831d7 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -111,6 +111,15 @@ GEM
rainbow (>= 3, < 4)
table_print (= 1.5.7)
zeitwerk (>= 2, < 3)
+ gitlab_quality-test_tooling (0.2.2)
+ activesupport (~> 6.1)
+ gitlab (~> 4.18.0)
+ http (~> 5.0)
+ nokogiri (~> 1.10)
+ parallel (>= 1, < 2)
+ rainbow (>= 3, < 4)
+ table_print (= 1.5.7)
+ zeitwerk (>= 2, < 3)
google-apis-compute_v1 (0.51.0)
google-apis-core (>= 0.7.2, < 2.a)
google-apis-core (0.9.0)
@@ -319,6 +328,7 @@ DEPENDENCIES
fog-core (= 2.1.0)
fog-google (~> 1.19)
gitlab-qa (~> 10, >= 10.3.0)
+ gitlab_quality-test_tooling (~> 0.2.2)
influxdb-client (~> 2.9)
knapsack (~> 4.0)
nokogiri (~> 1.14, >= 1.14.3)
diff --git a/scripts/allowed_warnings.txt b/scripts/allowed_warnings.txt
index 5310b806bbc..cc7d14c1d3c 100644
--- a/scripts/allowed_warnings.txt
+++ b/scripts/allowed_warnings.txt
@@ -22,3 +22,8 @@ ruby\/2\.7\.0\/net\/protocol\.rb:206: warning: previous definition of BUFSIZE wa
ruby\/2\.7\.0\/net\/protocol\.rb:503: warning: previous definition of Socket was here
2\.7\.0\/gems\/net-protocol-0\.1\.3\/lib\/net\/protocol\.rb:68: warning: already initialized constant Net::ProtocRetryError
ruby\/2\.7\.0\/net\/protocol\.rb:66: warning: previous definition of ProtocRetryError was here
+
+# Ruby 3 does not emit warnings for pattern matching, and if it's working
+# fine in both Ruby 2 and Ruby 3, it's unlikely it'll change again.
+# This can be removed when support for Ruby 2 is dropped.
+warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index b64e6ed6f58..4e73bf48021 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -13,9 +13,9 @@ function retrieve_tests_metadata() {
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi
- if [[ ! -f "${RSPEC_FAST_QUARANTINE_PATH}" ]]; then
- curl --location -o "${RSPEC_FAST_QUARANTINE_PATH}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_PATH}" ||
- echo "" > "${RSPEC_FAST_QUARANTINE_PATH}"
+ if [[ ! -f "${RSPEC_FAST_QUARANTINE_LOCAL_PATH}" ]]; then
+ curl --location -o "${RSPEC_FAST_QUARANTINE_LOCAL_PATH}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_LOCAL_PATH}" ||
+ echo "" > "${RSPEC_FAST_QUARANTINE_LOCAL_PATH}"
fi
}
@@ -139,7 +139,7 @@ function debug_rspec_variables() {
echoinfo "FLAKY_RSPEC_SUITE_REPORT_PATH: ${FLAKY_RSPEC_SUITE_REPORT_PATH:-}"
echoinfo "FLAKY_RSPEC_REPORT_PATH: ${FLAKY_RSPEC_REPORT_PATH:-}"
echoinfo "NEW_FLAKY_RSPEC_REPORT_PATH: ${NEW_FLAKY_RSPEC_REPORT_PATH:-}"
- echoinfo "SKIPPED_FLAKY_TESTS_REPORT_PATH: ${SKIPPED_FLAKY_TESTS_REPORT_PATH:-}"
+ echoinfo "SKIPPED_TESTS_REPORT_PATH: ${SKIPPED_TESTS_REPORT_PATH:-}"
echoinfo "CRYSTALBALL: ${CRYSTALBALL:-}"
@@ -205,7 +205,7 @@ function rspec_paralellized_job() {
export KNAPSACK_TEST_FILE_PATTERN=$(ruby -r./tooling/quality/test_level.rb -e "puts Quality::TestLevel.new(${spec_folder_prefixes}).pattern(:${test_level})")
export FLAKY_RSPEC_REPORT_PATH="${rspec_flaky_folder_path}all_${report_name}_report.json"
export NEW_FLAKY_RSPEC_REPORT_PATH="${rspec_flaky_folder_path}new_${report_name}_report.json"
- export SKIPPED_FLAKY_TESTS_REPORT_PATH="${rspec_flaky_folder_path}skipped_flaky_tests_${report_name}_report.txt"
+ export SKIPPED_TESTS_REPORT_PATH="rspec/skipped_tests_${report_name}.txt"
if [[ -d "ee/" ]]; then
export KNAPSACK_GENERATE_REPORT="true"
@@ -408,7 +408,7 @@ function generate_flaky_tests_reports() {
mkdir -p ${rspec_flaky_folder_path}
- find ${rspec_flaky_folder_path} -type f -name 'skipped_flaky_tests_*_report.txt' -exec cat {} + >> "${SKIPPED_FLAKY_TESTS_REPORT_PATH}"
+ find ${rspec_flaky_folder_path} -type f -name 'skipped_tests_*.txt' -exec cat {} + >> "${SKIPPED_TESTS_REPORT_PATH}"
find ${rspec_flaky_folder_path} -type f -name 'retried_tests_*_report.txt' -exec cat {} + >> "${RETRIED_TESTS_REPORT_PATH}"
cleanup_individual_job_reports
diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping
index c37ccafa02c..86ab7548b19 100755
--- a/scripts/verify-tff-mapping
+++ b/scripts/verify-tff-mapping
@@ -214,6 +214,12 @@ tests = [
explanation: 'https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/1360',
source: 'vendor/project_templates/gitbook.tar.gz',
expected: ['spec/lib/gitlab/project_template_spec.rb']
+ },
+
+ {
+ explanation: 'https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/1683#note_1385966977',
+ source: 'app/finders/members_finder.rb',
+ expected: ['spec/finders/members_finder_spec.rb', 'spec/graphql/types/project_member_relation_enum_spec.rb']
}
]
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
index 7c74511e5b4..fa20fc5037f 100644
--- a/spec/controllers/projects/grafana_api_controller_spec.rb
+++ b/spec/controllers/projects/grafana_api_controller_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe Projects::GrafanaApiController, feature_category: :metrics do
end
before do
+ stub_feature_flags(remove_monitor_metrics: false)
sign_in(user) if user
end
@@ -170,6 +171,14 @@ RSpec.describe Projects::GrafanaApiController, feature_category: :metrics do
it_behaves_like 'accessible'
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it_behaves_like 'not accessible'
+ end
end
describe 'GET #metrics_dashboard' do
diff --git a/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb b/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb
index dc59dda3322..02407e31756 100644
--- a/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb
+++ b/spec/controllers/projects/performance_monitoring/dashboards_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PerformanceMonitoring::DashboardsController do
+RSpec.describe Projects::PerformanceMonitoring::DashboardsController, feature_category: :metrics do
let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:namespace) }
@@ -25,6 +25,10 @@ RSpec.describe Projects::PerformanceMonitoring::DashboardsController do
}
end
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
describe 'POST #create' do
context 'authenticated user' do
before do
@@ -102,6 +106,18 @@ RSpec.describe Projects::PerformanceMonitoring::DashboardsController do
expect(json_response).to eq('error' => "Request parameter branch is missing.")
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 404 not found' do
+ post :create, params: params
+
+ expect(response).to have_gitlab_http_status :not_found
+ end
+ end
end
end
end
@@ -217,6 +233,18 @@ RSpec.describe Projects::PerformanceMonitoring::DashboardsController do
expect(json_response).to eq('error' => 'something went wrong')
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 404 not found' do
+ put :update, params: params
+
+ expect(response).to have_gitlab_http_status :not_found
+ end
+ end
end
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index a9605b214bd..560e8ddd07b 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -526,7 +526,7 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
end
describe 'when user has made changes' do
- it 'shows a warning and can leave page' do
+ it 'shows a warning and can leave page', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410497' do
content = 'new issue content'
find('body').send_keys('e')
fill_in 'issue-description', with: content
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index 9f00bb99c0d..1b68f78e993 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline, feature_category: :team_planning do
+RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline, feature_category: :metrics do
include PrometheusHelpers
include KubernetesHelpers
include GrafanaApiHelpers
@@ -29,6 +29,20 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
clear_host_from_memoized_variables
end
+ shared_examples_for 'metrics dashboard unavailable' do
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'shows no embedded metrics' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_no_css('div.prometheus-graph')
+ end
+ end
+ end
+
context 'internal metrics embeds' do
before do
import_common_metrics
@@ -37,6 +51,8 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
allow(Prometheus::ProxyService).to receive(:new).and_call_original
end
+ include_examples 'metrics dashboard unavailable'
+
it 'shows embedded metrics' do
visit project_issue_path(project, issue)
@@ -135,6 +151,8 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
allow(Grafana::ProxyService).to receive(:new).and_call_original
end
+ include_examples 'metrics dashboard unavailable'
+
it 'shows embedded metrics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/402973' do
visit project_issue_path(project, issue)
@@ -172,6 +190,8 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
stub_any_prometheus_request_with_response
end
+ include_examples 'metrics dashboard unavailable'
+
it 'shows embedded metrics' do
visit project_issue_path(project, issue)
@@ -201,6 +221,8 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
let(:metrics_url) { urls.namespace_project_cluster_url(*params, **query_params) }
let(:description) { "# Summary \n[](#{metrics_url})" }
+ include_examples 'metrics dashboard unavailable'
+
it 'shows embedded metrics' do
visit project_issue_path(project, issue)
diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb
index c5379cf56c0..9f061a2ff14 100644
--- a/spec/features/projects/blobs/blame_spec.rb
+++ b/spec/features/projects/blobs/blame_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do
it_behaves_like 'a full blame page'
- it 'shows loading text' do
+ it 'shows loading text', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410499' do
visit_blob_blame(path)
click_link _('Show full blame')
expect(page).to have_text('Loading full blame...')
diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb
index 20c1ef1b21f..9851194bd3c 100644
--- a/spec/features/projects/ci/editor_spec.rb
+++ b/spec/features/projects/ci/editor_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end
end
- it 'user who tries to navigate away can cancel the action and keep their changes' do
+ it 'user who tries to navigate away can cancel the action and keep their changes', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410496' do
click_link 'Pipelines'
page.driver.browser.switch_to.alert.dismiss
@@ -113,7 +113,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d
end
end
- it 'user who tries to navigate away can confirm the action and discard their change' do
+ it 'user who tries to navigate away can confirm the action and discard their change', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410496' do
click_link 'Pipelines'
page.driver.browser.switch_to.alert.accept
diff --git a/spec/finders/pending_todos_finder_spec.rb b/spec/finders/pending_todos_finder_spec.rb
index 4f4862852f4..0cf61a0958e 100644
--- a/spec/finders/pending_todos_finder_spec.rb
+++ b/spec/finders/pending_todos_finder_spec.rb
@@ -5,49 +5,60 @@ require 'spec_helper'
RSpec.describe PendingTodosFinder do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
let_it_be(:issue) { create(:issue) }
+ let_it_be(:issue2) { create(:issue) }
+ let_it_be(:project) { create(:project) }
let_it_be(:note) { create(:note) }
+ let_it_be(:todo) { create(:todo, :pending, user: user, target: issue) }
+ let_it_be(:todo2) { create(:todo, :pending, user: user, target: issue2, project: project) }
+ let_it_be(:todo3) { create(:todo, :pending, user: user2, target: issue) }
+ let_it_be(:todo4) { create(:todo, :pending, user: user3, target: issue) }
+ let_it_be(:done_todo) { create(:todo, :done, user: user) }
let(:users) { [user, user2] }
describe '#execute' do
- it 'returns only pending todos' do
- create(:todo, :done, user: user)
+ it 'returns all pending todos if no params are passed' do
+ todos = described_class.new.execute
- todo = create(:todo, :pending, user: user)
- todos = described_class.new(users).execute
+ expect(todos).to match_array([todo, todo2, todo3, todo4])
+ end
- expect(todos).to eq([todo])
+ it 'supports retrieving only pending todos for chosen users' do
+ todos = described_class.new(users: users).execute
+
+ expect(todos).to match_array([todo, todo2, todo3])
end
it 'supports retrieving of todos for a specific project' do
- project1 = create(:project)
project2 = create(:project)
+ project2_todo = create(:todo, :pending, user: user, project: project2)
- create(:todo, :pending, user: user, project: project2)
+ todos = described_class.new(users: user, project_id: project.id).execute
+ expect(todos).to match_array([todo2])
- todo = create(:todo, :pending, user: user, project: project1)
- todos = described_class.new(users, project_id: project1.id).execute
-
- expect(todos).to eq([todo])
+ todos = described_class.new(users: user, project_id: project2.id).execute
+ expect(todos).to match_array([project2_todo])
end
it 'supports retrieving of todos for a specific todo target' do
- todo = create(:todo, :pending, user: user, target: issue)
+ todos = described_class.new(users: user, target_id: issue.id, target_type: 'Issue').execute
- create(:todo, :pending, user: user, target: note)
-
- todos = described_class.new(users, target_id: issue.id, target_type: 'Issue').execute
-
- expect(todos).to eq([todo])
+ expect(todos).to match_array([todo])
end
it 'supports retrieving of todos for a specific target type' do
- todo = create(:todo, :pending, user: user, target: issue)
+ todos = described_class.new(users: user, target_type: issue.class.name).execute
+
+ expect(todos).to match_array([todo, todo2])
+ end
- create(:todo, :pending, user: user, target: note)
+ it 'supports retrieving of todos from a specific author' do
+ todo = create(:todo, :pending, user: user, author: user2, target: issue)
+ create(:todo, :pending, user: user, author: user3, target: issue)
- todos = described_class.new(users, target_type: issue.class.name).execute
+ todos = described_class.new(users: users, author_id: user2.id).execute
expect(todos).to eq([todo])
end
@@ -56,7 +67,7 @@ RSpec.describe PendingTodosFinder do
create(:todo, :pending, user: user, commit_id: '456')
todo = create(:todo, :pending, user: user, commit_id: '123')
- todos = described_class.new(users, commit_id: '123').execute
+ todos = described_class.new(users: users, commit_id: '123').execute
expect(todos).to eq([todo])
end
@@ -71,7 +82,7 @@ RSpec.describe PendingTodosFinder do
discussion = Discussion.lazy_find(first_discussion_note.discussion_id)
users = [note_2.author, note_3.author, user]
- todos = described_class.new(users, discussion: discussion).execute
+ todos = described_class.new(users: users, discussion: discussion).execute
expect(todos).to contain_exactly(todo1, todo2)
end
@@ -81,7 +92,7 @@ RSpec.describe PendingTodosFinder do
create(:todo, :pending, user: user, target: issue, action: Todo::ASSIGNED)
- todos = described_class.new(users, action: Todo::MENTIONED).execute
+ todos = described_class.new(users: users, action: Todo::MENTIONED).execute
expect(todos).to contain_exactly(todo)
end
diff --git a/spec/frontend/blame/blame_redirect_spec.js b/spec/frontend/blame/blame_redirect_spec.js
index 326f60a5b13..5cd91ec5f1f 100644
--- a/spec/frontend/blame/blame_redirect_spec.js
+++ b/spec/frontend/blame/blame_redirect_spec.js
@@ -6,7 +6,6 @@ jest.mock('~/alert');
describe('Blame page redirect', () => {
beforeEach(() => {
- global.window = Object.create(window);
const url = 'https://gitlab.com/flightjs/Flight/-/blame/master/file.json';
Object.defineProperty(window, 'location', {
writable: true,
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index 89d507b4ec5..6a7ca3288cb 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -11,6 +11,13 @@ jest.mock('~/blob_edit/edit_blob');
jest.mock('~/alert');
describe('BlobBundle', () => {
+ beforeAll(() => {
+ // HACK: Workaround readonly property in Jest
+ Object.defineProperty(window, 'onbeforeunload', {
+ writable: true,
+ });
+ });
+
it('does not load SourceEditor by default', () => {
blobBundle();
expect(SourceEditor).not.toHaveBeenCalled();
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index 8492fe7bdde..c0cfec384f0 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -1,9 +1,9 @@
import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'spec/test_constants';
import ConfigureFeatureFlagsModal from '~/feature_flags/components/configure_feature_flags_modal.vue';
import EmptyState from '~/feature_flags/components/empty_state.vue';
@@ -11,7 +11,7 @@ import FeatureFlagsComponent from '~/feature_flags/components/feature_flags.vue'
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
import createStore from '~/feature_flags/store/index';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getRequestData } from '../mock_data';
@@ -43,7 +43,7 @@ describe('Feature flags', () => {
let mock;
let store;
- const factory = (provide = mockData, fn = mount) => {
+ const factory = (provide = mockData, fn = mountExtended) => {
store = createStore(mockState);
wrapper = fn(FeatureFlagsComponent, {
store,
@@ -54,10 +54,13 @@ describe('Feature flags', () => {
});
};
- const configureButton = () => wrapper.find('[data-testid="ff-configure-button"]');
- const newButton = () => wrapper.find('[data-testid="ff-new-button"]');
- const userListButton = () => wrapper.find('[data-testid="ff-user-list-button"]');
+ const configureButton = () => wrapper.findByTestId('ff-configure-button');
+ const newButton = () => wrapper.findByTestId('ff-new-button');
+ const userListButton = () => wrapper.findByTestId('ff-user-list-button');
const limitAlert = () => wrapper.findComponent(GlAlert);
+ const findTablePagination = () => wrapper.findComponent(TablePagination);
+ const findFeatureFlagsTable = () => wrapper.findComponent(FeatureFlagsTable);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -81,7 +84,7 @@ describe('Feature flags', () => {
it('makes the new feature flag button do nothing if clicked', () => {
expect(newButton().exists()).toBe(true);
expect(newButton().props('disabled')).toBe(false);
- expect(newButton().props('href')).toBe(undefined);
+ expect(newButton().props('href')).toBeUndefined();
});
it('shows a feature flags limit reached alert', () => {
@@ -171,9 +174,8 @@ describe('Feature flags', () => {
factory();
await waitForPromises();
- await nextTick();
- emptyState = wrapper.findComponent(GlEmptyState);
+ emptyState = findEmptyState();
});
it('should render the empty state', () => {
@@ -219,7 +221,7 @@ describe('Feature flags', () => {
});
it('should render a table with feature flags', () => {
- const table = wrapper.findComponent(FeatureFlagsTable);
+ const table = findFeatureFlagsTable();
expect(table.exists()).toBe(true);
expect(table.props('featureFlags')).toEqual(
expect.arrayContaining([
@@ -232,7 +234,7 @@ describe('Feature flags', () => {
});
it('should toggle a flag when receiving the toggle-flag event', () => {
- const table = wrapper.findComponent(FeatureFlagsTable);
+ const table = findFeatureFlagsTable();
const [flag] = table.props('featureFlags');
table.vm.$emit('toggle-flag', flag);
@@ -255,15 +257,15 @@ describe('Feature flags', () => {
describe('pagination', () => {
it('should render pagination', () => {
- expect(wrapper.findComponent(TablePagination).exists()).toBe(true);
+ expect(findTablePagination().exists()).toBe(true);
});
it('should make an API request when page is clicked', () => {
- jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions');
- wrapper.findComponent(TablePagination).vm.change(4);
+ const axiosGet = jest.spyOn(axios, 'get');
+ findTablePagination().vm.change(4);
- expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
- page: '4',
+ expect(axiosGet).toHaveBeenCalledWith('http://test.host/endpoint.json', {
+ params: { page: '4' },
});
});
});
@@ -272,16 +274,12 @@ describe('Feature flags', () => {
describe('unsuccessful request', () => {
beforeEach(() => {
- mock
- .onGet(mockState.endpoint, { params: { page: '1' } })
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
-
factory();
return waitForPromises();
});
it('should render error state', () => {
- const emptyState = wrapper.findComponent(GlEmptyState);
+ const emptyState = findEmptyState();
expect(emptyState.props('title')).toEqual('There was an error fetching the feature flags.');
expect(emptyState.props('description')).toEqual(
'Try again in a few moments or contact your support team.',
@@ -303,20 +301,12 @@ describe('Feature flags', () => {
});
describe('rotate instance id', () => {
- beforeEach(() => {
- mock
- .onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(HTTP_STATUS_OK, getRequestData, {});
- factory();
- return waitForPromises();
- });
-
it('should fire the rotate action when a `token` event is received', () => {
- const actionSpy = jest.spyOn(wrapper.vm, 'rotateInstanceId');
- const modal = wrapper.findComponent(ConfigureFeatureFlagsModal);
- modal.vm.$emit('token');
+ factory();
+ const axiosPost = jest.spyOn(axios, 'post');
+ wrapper.findComponent(ConfigureFeatureFlagsModal).vm.$emit('token');
- expect(actionSpy).toHaveBeenCalled();
+ expect(axiosPost).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index f2a684ab65e..eb8f2a5e4ac 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -45,6 +45,13 @@ describe('WebIDE', () => {
const callOnBeforeUnload = (e = {}) => window.onbeforeunload(e);
+ beforeAll(() => {
+ // HACK: Workaround readonly property in Jest
+ Object.defineProperty(window, 'onbeforeunload', {
+ writable: true,
+ });
+ });
+
beforeEach(() => {
stubPerformanceWebAPI();
diff --git a/spec/frontend/notes/components/mr_discussion_filter_spec.js b/spec/frontend/notes/components/mr_discussion_filter_spec.js
index 405043ff2a0..beb25c30af6 100644
--- a/spec/frontend/notes/components/mr_discussion_filter_spec.js
+++ b/spec/frontend/notes/components/mr_discussion_filter_spec.js
@@ -80,15 +80,15 @@ describe('Merge request discussion filter component', () => {
wrapper.findComponent(GlCollapsibleListbox).vm.$emit('hidden');
expect(updateMergeRequestFilters).toHaveBeenCalledWith(expect.anything(), [
- 'commit_branches',
- 'status',
'assignees_reviewers',
+ 'comments',
+ 'commit_branches',
'edits',
'labels',
+ 'lock_status',
'mentions',
+ 'status',
'tracking',
- 'comments',
- 'lock_status',
]);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index e78e1be7882..a07a60438fb 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -2,6 +2,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -16,6 +17,7 @@ import {
} from '~/vue_merge_request_widget/components/approvals/messages';
import eventHub from '~/vue_merge_request_widget/event_hub';
import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
+import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
import { createCanApproveResponse } from 'jest/approvals/mock_data';
Vue.use(VueApollo);
@@ -42,21 +44,36 @@ const testApprovals = () => ({
});
describe('MRWidget approvals', () => {
+ let mockedSubscription;
let wrapper;
let service;
let mr;
- const createComponent = (props = {}, response = approvedByCurrentUser) => {
- const requestHandlers = [[approvedByQuery, jest.fn().mockResolvedValue(response)]];
+ const createComponent = (options = {}, responses = { query: approvedByCurrentUser }) => {
+ mockedSubscription = createMockApolloSubscription();
+
+ const requestHandlers = [[approvedByQuery, jest.fn().mockResolvedValue(responses.query)]];
+ const subscriptionHandlers = [[approvedBySubscription, () => mockedSubscription]];
const apolloProvider = createMockApollo(requestHandlers);
+ const provide = {
+ ...options.provide,
+ glFeatures: {
+ realtimeApprovals: options.provide?.glFeatures?.realtimeApprovals || false,
+ },
+ };
+
+ subscriptionHandlers.forEach(([document, stream]) => {
+ apolloProvider.defaultClient.setRequestHandler(document, stream);
+ });
wrapper = shallowMount(Approvals, {
apolloProvider,
propsData: {
mr,
service,
- ...props,
+ ...options.props,
},
+ provide,
stubs: {
GlSprintf,
},
@@ -97,6 +114,7 @@ describe('MRWidget approvals', () => {
isOpen: true,
state: 'open',
targetProjectFullPath: 'gitlab-org/gitlab',
+ id: 1,
iid: '1',
};
@@ -114,7 +132,7 @@ describe('MRWidget approvals', () => {
mr.isOpen = false;
- createComponent({}, response);
+ createComponent({}, { query: response });
await waitForPromises();
});
@@ -128,7 +146,7 @@ describe('MRWidget approvals', () => {
const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
response.data.project.mergeRequest.approvedBy.nodes = [];
- createComponent({}, response);
+ createComponent({}, { query: response });
await waitForPromises();
});
@@ -146,7 +164,7 @@ describe('MRWidget approvals', () => {
describe('and MR is unapproved', () => {
beforeEach(async () => {
- createComponent({}, canApproveResponse);
+ createComponent({}, { query: canApproveResponse });
await waitForPromises();
});
@@ -167,7 +185,7 @@ describe('MRWidget approvals', () => {
describe('with no approvers', () => {
beforeEach(async () => {
canApproveResponse.data.project.mergeRequest.approvedBy.nodes = [];
- createComponent({}, canApproveResponse);
+ createComponent({}, { query: canApproveResponse });
await nextTick();
});
@@ -186,7 +204,7 @@ describe('MRWidget approvals', () => {
canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 2;
- createComponent({}, canApproveResponse);
+ createComponent({}, { query: canApproveResponse });
await waitForPromises();
});
@@ -202,7 +220,7 @@ describe('MRWidget approvals', () => {
describe('when approve action is clicked', () => {
beforeEach(async () => {
- createComponent({}, canApproveResponse);
+ createComponent({}, { query: canApproveResponse });
await waitForPromises();
});
@@ -259,7 +277,7 @@ describe('MRWidget approvals', () => {
beforeEach(async () => {
const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
- createComponent({}, response);
+ createComponent({}, { query: response });
await waitForPromises();
});
@@ -320,7 +338,7 @@ describe('MRWidget approvals', () => {
beforeEach(async () => {
optionalApprovalsResponse.data.project.mergeRequest.userPermissions.canApprove = true;
- createComponent({}, optionalApprovalsResponse);
+ createComponent({}, { query: optionalApprovalsResponse });
await waitForPromises();
});
@@ -335,7 +353,7 @@ describe('MRWidget approvals', () => {
describe('and cannot approve', () => {
beforeEach(async () => {
- createComponent({}, optionalApprovalsResponse);
+ createComponent({}, { query: optionalApprovalsResponse });
await nextTick();
});
@@ -366,4 +384,44 @@ describe('MRWidget approvals', () => {
});
});
});
+
+ describe('realtime approvals update', () => {
+ describe('realtime_approvals feature disabled', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'warn').mockImplementation();
+ createComponent();
+ });
+
+ it('does not subscribe to the approvals update socket', () => {
+ expect(mr.setApprovals).not.toHaveBeenCalled();
+ mockedSubscription.next({});
+ // eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalledWith(
+ expect.stringMatching('Mock subscription has no observer, this will have no effect'),
+ );
+ expect(mr.setApprovals).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('realtime_approvals feature enabled', () => {
+ const subscriptionApproval = { approved: true };
+ const subscriptionResponse = {
+ data: { mergeRequestApprovalStateUpdated: subscriptionApproval },
+ };
+
+ beforeEach(() => {
+ createComponent({
+ provide: { glFeatures: { realtimeApprovals: true } },
+ });
+ });
+
+ it('updates approvals when the subscription data is streamed to the Apollo client', () => {
+ expect(mr.setApprovals).not.toHaveBeenCalled();
+
+ mockedSubscription.next(subscriptionResponse);
+
+ expect(mr.setApprovals).toHaveBeenCalledWith(subscriptionApproval);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 1bdf5d1c840..e305cc310bd 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -6,21 +6,16 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
-import {
- deleteWorkItemFromTaskMutationErrorResponse,
- deleteWorkItemFromTaskMutationResponse,
- deleteWorkItemMutationErrorResponse,
- deleteWorkItemResponse,
-} from '../mock_data';
+import { deleteWorkItemMutationErrorResponse, deleteWorkItemResponse } from '../mock_data';
describe('WorkItemDetailModal component', () => {
let wrapper;
Vue.use(VueApollo);
+ const workItemId = 'gid://gitlab/WorkItem/1';
const hideModal = jest.fn();
const GlModal = {
template: `
@@ -33,37 +28,23 @@ describe('WorkItemDetailModal component', () => {
},
};
- const defaultPropsData = {
- issueGid: 'gid://gitlab/WorkItem/1',
- workItemId: 'gid://gitlab/WorkItem/2',
- };
-
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
const createComponent = ({
- lockVersion,
- lineNumberStart,
- lineNumberEnd,
error = false,
- deleteWorkItemFromTaskMutationHandler = jest
- .fn()
- .mockResolvedValue(deleteWorkItemFromTaskMutationResponse),
deleteWorkItemMutationHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
} = {}) => {
const apolloProvider = createMockApollo([
- [deleteWorkItemFromTaskMutation, deleteWorkItemFromTaskMutationHandler],
[deleteWorkItemMutation, deleteWorkItemMutationHandler],
]);
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider,
propsData: {
- ...defaultPropsData,
- lockVersion,
- lineNumberStart,
- lineNumberEnd,
+ workItemId,
+ workItemIid: '1',
},
data() {
return {
@@ -87,9 +68,9 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
- workItemId: defaultPropsData.workItemId,
- workItemParentId: defaultPropsData.issueGid,
- workItemIid: null,
+ workItemId,
+ workItemIid: '1',
+ workItemParentId: null,
});
});
@@ -143,85 +124,31 @@ describe('WorkItemDetailModal component', () => {
});
describe('delete work item', () => {
- describe('when there is task data', () => {
- it('emits workItemDeleted and closes modal', async () => {
- const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationResponse);
- createComponent({
- lockVersion: 1,
- lineNumberStart: '3',
- lineNumberEnd: '3',
- deleteWorkItemFromTaskMutationHandler: mutationMock,
- });
- const newDesc = 'updated work item desc';
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
- expect(hideModal).toHaveBeenCalled();
- expect(mutationMock).toHaveBeenCalledWith({
- input: {
- id: defaultPropsData.issueGid,
- lockVersion: 1,
- taskData: { id: defaultPropsData.workItemId, lineNumberEnd: 3, lineNumberStart: 3 },
- },
- });
- });
-
- it.each`
- errorType | mutationMock | errorMessage
- ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationErrorResponse)} | ${'Error'}
- ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
- `(
- 'shows an error message when there is $errorType',
- async ({ mutationMock, errorMessage }) => {
- createComponent({
- lockVersion: 1,
- lineNumberStart: '3',
- lineNumberEnd: '3',
- deleteWorkItemFromTaskMutationHandler: mutationMock,
- });
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
- expect(hideModal).not.toHaveBeenCalled();
- expect(findAlert().text()).toBe(errorMessage);
- },
- );
+ it('emits workItemDeleted and closes modal', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse);
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[workItemId]]);
+ expect(hideModal).toHaveBeenCalled();
+ expect(mutationMock).toHaveBeenCalledWith({ input: { id: workItemId } });
});
- describe('when there is no task data', () => {
- it('emits workItemDeleted and closes modal', async () => {
- const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse);
- createComponent({ deleteWorkItemMutationHandler: mutationMock });
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toEqual([[defaultPropsData.workItemId]]);
- expect(hideModal).toHaveBeenCalled();
- expect(mutationMock).toHaveBeenCalledWith({ input: { id: defaultPropsData.workItemId } });
- });
-
- it.each`
- errorType | mutationMock | errorMessage
- ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'}
- ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
- `(
- 'shows an error message when there is $errorType',
- async ({ mutationMock, errorMessage }) => {
- createComponent({ deleteWorkItemMutationHandler: mutationMock });
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
- expect(hideModal).not.toHaveBeenCalled();
- expect(findAlert().text()).toBe(errorMessage);
- },
- );
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
+ `('shows an error message when there is $errorType', async ({ mutationMock, errorMessage }) => {
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(hideModal).not.toHaveBeenCalled();
+ expect(findAlert().text()).toBe(errorMessage);
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
index 08b9408c656..b06be6c8083 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
@@ -53,11 +53,12 @@ describe('WorkItemChildrenWrapper', () => {
it('remove event on child triggers `removeChild` event', () => {
createComponent();
+ const workItem = { id: 'gid://gitlab/WorkItem/2' };
const firstChild = findWorkItemLinkChildItems().at(0);
- firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
+ firstChild.vm.$emit('removeChild', workItem);
- expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
+ expect(wrapper.emitted('removeChild')).toEqual([[workItem]]);
});
it('emits `show-modal` on `click` event', () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index bc429bfb037..71d1a0e253f 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -236,7 +236,7 @@ describe('WorkItemLinkChild', () => {
it('removeChild event on menu triggers `click-remove-child` event', () => {
itemMenuEl.vm.$emit('removeChild');
- expect(wrapper.emitted('removeChild')).toEqual([[workItemTask.id]]);
+ expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
});
});
@@ -249,7 +249,7 @@ describe('WorkItemLinkChild', () => {
(widget) => widget.type === WIDGET_TYPE_HIERARCHY,
);
const getChildrenNodes = () => getWidgetHierarchy().children.nodes;
- const findFirstItemId = () => getChildrenNodes()[0].id;
+ const findFirstItem = () => getChildrenNodes()[0];
beforeEach(() => {
getWorkItemTreeQueryHandler.mockClear();
@@ -328,7 +328,7 @@ describe('WorkItemLinkChild', () => {
findExpandButton().vm.$emit('click');
await waitForPromises();
- findTreeChildren().vm.$emit('removeChild', findFirstItemId());
+ findTreeChildren().vm.$emit('removeChild', findFirstItem());
await waitForPromises();
expect($toast.show).toHaveBeenCalledWith('Child removed', {
@@ -343,23 +343,23 @@ describe('WorkItemLinkChild', () => {
const childrenNodes = getChildrenNodes();
expect(findTreeChildren().props('children')).toEqual(childrenNodes);
- findTreeChildren().vm.$emit('removeChild', findFirstItemId());
+ findTreeChildren().vm.$emit('removeChild', findFirstItem());
await waitForPromises();
expect(findTreeChildren().props('children')).toEqual([]);
});
it('calls correct mutation with correct variables', async () => {
- const firstItemId = findFirstItemId();
+ const firstItem = findFirstItem();
findExpandButton().vm.$emit('click');
await waitForPromises();
- findTreeChildren().vm.$emit('removeChild', firstItemId);
+ findTreeChildren().vm.$emit('removeChild', firstItem);
expect(mutationChangeParentHandler).toHaveBeenCalledWith({
input: {
- id: firstItemId,
+ id: firstItem.id,
hierarchyWidget: {
parentId: null,
},
@@ -383,7 +383,7 @@ describe('WorkItemLinkChild', () => {
findExpandButton().vm.$emit('click');
await waitForPromises();
- findTreeChildren().vm.$emit('removeChild', findFirstItemId());
+ findTreeChildren().vm.$emit('removeChild', findFirstItem());
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index cc3d2394231..786f8604039 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -240,7 +240,7 @@ describe('WorkItemLinks', () => {
});
it('calls correct mutation with correct variables', async () => {
- findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild.id);
+ findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
await waitForPromises();
@@ -255,7 +255,7 @@ describe('WorkItemLinks', () => {
});
it('shows toast when mutation succeeds', async () => {
- findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild.id);
+ findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
await waitForPromises();
@@ -267,7 +267,7 @@ describe('WorkItemLinks', () => {
it('renders correct number of children after removal', async () => {
expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
- findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild.id);
+ findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
await waitForPromises();
expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(3);
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 0a0835bcb36..0d5f92d0f4a 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -714,24 +714,6 @@ export const deleteWorkItemMutationErrorResponse = {
},
};
-export const deleteWorkItemFromTaskMutationResponse = {
- data: {
- workItemDeleteTask: {
- workItem: { id: 123, descriptionHtml: 'updated work item desc' },
- errors: [],
- },
- },
-};
-
-export const deleteWorkItemFromTaskMutationErrorResponse = {
- data: {
- workItemDeleteTask: {
- workItem: { id: 123, descriptionHtml: 'updated work item desc' },
- errors: ['Error'],
- },
- },
-};
-
export const workItemDatesSubscriptionResponse = {
data: {
issuableDatesUpdated: {
diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb
index 489d9d3fcee..a9ab4ab3b67 100644
--- a/spec/helpers/ci/jobs_helper_spec.rb
+++ b/spec/helpers/ci/jobs_helper_spec.rb
@@ -3,24 +3,49 @@
require 'spec_helper'
RSpec.describe Ci::JobsHelper do
- describe 'jobs data' do
- let(:project) { create(:project, :repository) }
- let(:bridge) { create(:ci_bridge) }
-
- subject(:bridge_data) { helper.bridge_data(bridge, project) }
+ describe 'job helper functions' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:job) { create(:ci_build, project: project) }
before do
- allow(helper)
- .to receive(:image_path)
- .and_return('/path/to/illustration')
+ helper.instance_variable_set(:@project, project)
+ helper.instance_variable_set(:@build, job)
+ end
+
+ it 'returns jobs data' do
+ expect(helper.jobs_data).to include({
+ "endpoint" => "/#{project.full_path}/-/jobs/#{job.id}.json",
+ "project_path" => project.full_path,
+ "artifact_help_url" => "/help/user/gitlab_com/index.md#gitlab-cicd",
+ "deployment_help_url" => "/help/user/project/clusters/deploy_to_cluster.md#troubleshooting",
+ "runner_settings_url" => "/#{project.full_path}/-/runners#js-runners-settings",
+ "page_path" => "/#{project.full_path}/-/jobs/#{job.id}",
+ "build_status" => "pending",
+ "build_stage" => "test",
+ "log_state" => "",
+ "build_options" => {
+ build_stage: "test",
+ build_status: "pending",
+ log_state: "",
+ page_path: "/#{project.full_path}/-/jobs/#{job.id}"
+ },
+ "retry_outdated_job_docs_url" => "/help/ci/pipelines/settings#retry-outdated-jobs"
+ })
end
- it 'returns bridge data' do
- expect(bridge_data).to eq({
- "build_id" => bridge.id,
- "empty-state-illustration-path" => '/path/to/illustration',
- "pipeline_iid" => bridge.pipeline.iid,
- "project_full_path" => project.full_path
+ it 'returns job statuses' do
+ expect(helper.job_statuses).to eq({
+ "canceled" => "CANCELED",
+ "created" => "CREATED",
+ "failed" => "FAILED",
+ "manual" => "MANUAL",
+ "pending" => "PENDING",
+ "preparing" => "PREPARING",
+ "running" => "RUNNING",
+ "scheduled" => "SCHEDULED",
+ "skipped" => "SKIPPED",
+ "success" => "SUCCESS",
+ "waiting_for_resource" => "WAITING_FOR_RESOURCE"
})
end
end
diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
index db0c10a802b..746fa6c48a5 100644
--- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do
+RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter, feature_category: :metrics do
include FilterSpecHelper
let_it_be(:project) { create(:project) }
@@ -29,6 +29,10 @@ RSpec.describe Banzai::Filter::InlineGrafanaMetricsFilter do
)
end
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
around do |example|
travel_to(Time.utc(2019, 3, 17, 13, 10)) { example.run }
end
diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/manage_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/manage_menu_spec.rb
index cde9ab0d6fe..916d0942db2 100644
--- a/spec/lib/sidebars/groups/super_sidebar_menus/manage_menu_spec.rb
+++ b/spec/lib/sidebars/groups/super_sidebar_menus/manage_menu_spec.rb
@@ -17,9 +17,7 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::ManageMenu, feature_category
expect(items.map(&:item_id)).to eq([
:activity,
:members,
- :labels,
- :milestones,
- :iterations
+ :labels
])
end
end
diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/plan_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/plan_menu_spec.rb
index 1d228a455ec..1ac2cf87236 100644
--- a/spec/lib/sidebars/groups/super_sidebar_menus/plan_menu_spec.rb
+++ b/spec/lib/sidebars/groups/super_sidebar_menus/plan_menu_spec.rb
@@ -20,6 +20,8 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::PlanMenu, feature_category:
:issue_boards,
:epic_boards,
:roadmap,
+ :milestones,
+ :iterations,
:group_wiki,
:crm_contacts,
:crm_organizations
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/manage_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/manage_menu_spec.rb
index 6a6d61496ea..afcdf2550d7 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/manage_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/manage_menu_spec.rb
@@ -17,9 +17,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::ManageMenu, feature_catego
expect(items.map(&:item_id)).to eq([
:activity,
:members,
- :labels,
- :milestones,
- :iterations
+ :labels
])
end
end
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
index 5c7f11bafe5..9344bbc76db 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/monitor_menu_spec.rb
@@ -20,7 +20,8 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::MonitorMenu, feature_categ
:alert_management,
:incidents,
:on_call_schedules,
- :escalation_policies
+ :escalation_policies,
+ :service_desk
])
end
end
diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb
index 57e6950dd69..8d61c9d9a0e 100644
--- a/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb
+++ b/spec/lib/sidebars/projects/super_sidebar_menus/plan_menu_spec.rb
@@ -17,8 +17,9 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::PlanMenu, feature_category
expect(items.map(&:item_id)).to eq([
:project_issue_list,
:boards,
+ :milestones,
+ :iterations,
:project_wiki,
- :service_desk,
:requirements
])
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index e0bfe41a3ae..98bfb3366d2 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -25,14 +25,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { expect(setting.kroki_formats).to eq({}) }
end
- describe 'associations' do
- it do
- is_expected.to belong_to(:instance_group).class_name('Group')
- .with_foreign_key(:instance_administrators_group_id)
- .inverse_of(:application_setting)
- end
- end
-
describe 'validations' do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 9e8b4032ee3..67e4e128019 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -41,11 +41,6 @@ RSpec.describe Group, feature_category: :subgroups do
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
- it do
- is_expected.to have_many(:application_setting)
- .with_foreign_key(:instance_administrators_group_id).inverse_of(:instance_group)
- end
-
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
it do
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index f1bc7b41cee..7a46e5e7e53 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe Key, :mailer do
.with(key)
.and_return(service)
- expect(service).to receive(:execute)
+ expect(service).to receive(:execute_async)
key.update_last_used_at
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 3291eba48d3..e79459e0c7c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -1248,8 +1248,18 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
let_it_be(:first_build_info) { create(:package_build_info, :with_pipeline, package: package) }
let_it_be(:second_build_info) { create(:package_build_info, :with_pipeline, package: package) }
- it 'returns the first build info' do
- expect(package.original_build_info).to eq(first_build_info)
+ it 'returns the last build info' do
+ expect(package.original_build_info).to eq(second_build_info)
+ end
+
+ context 'with packages_display_last_pipeline disabled' do
+ before do
+ stub_feature_flags(packages_display_last_pipeline: false)
+ end
+
+ it 'returns the first build info' do
+ expect(package.original_build_info).to eq(first_build_info)
+ end
end
end
end
diff --git a/spec/requests/projects/metrics/dashboards/builder_spec.rb b/spec/requests/projects/metrics/dashboards/builder_spec.rb
index c929beaed70..8af2d1f1d25 100644
--- a/spec/requests/projects/metrics/dashboards/builder_spec.rb
+++ b/spec/requests/projects/metrics/dashboards/builder_spec.rb
@@ -49,6 +49,10 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_categ
end
describe 'POST /:namespace/:project/-/metrics/dashboards/builder' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
context 'as anonymous user' do
it 'redirects user to sign in page' do
send_request
@@ -102,6 +106,18 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_categ
expect(json_response['message']).to eq('Invalid configuration format')
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns not found' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
end
end
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_test1_and_test2_have_when.yml b/spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_test1_and_test2_have_when.yml
new file mode 100644
index 00000000000..cc92aaba679
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_test1_and_test2_have_when.yml
@@ -0,0 +1,46 @@
+config:
+ build:
+ stage: build
+ script: sleep 10
+
+ test1:
+ stage: test
+ script: exit 0
+ when: on_success
+
+ test2:
+ stage: test
+ script: exit 0
+ when: on_failure
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test1: created
+ test2: created
+ deploy: created
+
+transitions:
+ - event: cancel
+ jobs: [build]
+ expect:
+ pipeline: canceled
+ stages:
+ build: canceled
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: canceled
+ test1: skipped
+ test2: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_with_allow_failure_test1_and_test2_have_when.yml b/spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_with_allow_failure_test1_and_test2_have_when.yml
new file mode 100644
index 00000000000..34f01afe1de
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_build_cancels_with_allow_failure_test1_and_test2_have_when.yml
@@ -0,0 +1,47 @@
+config:
+ build:
+ stage: build
+ script: sleep 10
+ allow_failure: true
+
+ test1:
+ stage: test
+ script: exit 0
+ when: on_success
+
+ test2:
+ stage: test
+ script: exit 0
+ when: on_failure
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test1: created
+ test2: created
+ deploy: created
+
+transitions:
+ - event: cancel
+ jobs: [build]
+ expect:
+ pipeline: pending
+ stages:
+ build: success
+ test: pending
+ deploy: created
+ jobs:
+ build: canceled
+ test1: pending
+ test2: skipped
+ deploy: created
diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb
index 3113fe27acf..32100d793ff 100644
--- a/spec/services/keys/last_used_service_spec.rb
+++ b/spec/services/keys/last_used_service_spec.rb
@@ -4,31 +4,49 @@ require 'spec_helper'
RSpec.describe Keys::LastUsedService, feature_category: :source_code_management do
describe '#execute', :clean_gitlab_redis_shared_state do
- it 'updates the key when it has not been used recently' do
- key = create(:key, last_used_at: 1.year.ago)
- time = Time.zone.now
+ context 'when it has not been used recently' do
+ let(:key) { create(:key, last_used_at: 1.year.ago) }
+ let(:time) { Time.zone.now }
- travel_to(time) { described_class.new(key).execute }
+ it 'updates the key' do
+ travel_to(time) { described_class.new(key).execute }
- expect(key.reload.last_used_at).to be_like_time(time)
+ expect(key.reload.last_used_at).to be_like_time(time)
+ end
end
- it 'does not update the key when it has been used recently' do
- time = 1.minute.ago
- key = create(:key, last_used_at: time)
+ context 'when it has been used recently' do
+ let(:time) { 1.minute.ago }
+ let(:key) { create(:key, last_used_at: time) }
- described_class.new(key).execute
+ it 'does not update the key' do
+ described_class.new(key).execute
- expect(key.last_used_at).to be_like_time(time)
+ expect(key.reload.last_used_at).to be_like_time(time)
+ end
end
+ end
+
+ describe '#execute_async', :clean_gitlab_redis_shared_state do
+ context 'when it has not been used recently' do
+ let(:key) { create(:key, last_used_at: 1.year.ago) }
+ let(:time) { Time.zone.now }
- it 'does not update the updated_at field' do
- # Since a lot of these updates could happen in parallel for different keys
- # we want these updates to be as lightweight as possible, hence we want to
- # make sure we _only_ update last_used_at and not always updated_at.
- key = create(:key, last_used_at: 1.year.ago)
+ it 'schedules a job to update last_used_at' do
+ expect(::SshKeys::UpdateLastUsedAtWorker).to receive(:perform_async)
- expect { described_class.new(key).execute }.not_to change { key.updated_at }
+ travel_to(time) { described_class.new(key).execute_async }
+ end
+ end
+
+ context 'when it has been used recently' do
+ let(:key) { create(:key, last_used_at: 1.minute.ago) }
+
+ it 'does not schedule a job to update last_used_at' do
+ expect(::SshKeys::UpdateLastUsedAtWorker).not_to receive(:perform_async)
+
+ described_class.new(key).execute_async
+ end
end
end
@@ -47,14 +65,6 @@ RSpec.describe Keys::LastUsedService, feature_category: :source_code_management
expect(service.update?).to eq(true)
end
- it 'returns false when a lease has already been obtained' do
- key = build(:key, last_used_at: 1.year.ago)
- service = described_class.new(key)
-
- expect(service.update?).to eq(true)
- expect(service.update?).to eq(false)
- end
-
it 'returns false when the key does not yet need to be updated' do
key = build(:key, last_used_at: 1.minute.ago)
service = described_class.new(key)
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index de5074809cb..6c0d47e98ba 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -14,13 +14,17 @@ RSpec.describe Members::ApproveAccessRequestService, feature_category: :subgroup
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(current_user, params).execute(access_requester, **opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect do
+ described_class.new(current_user, params).execute(access_requester, **opts)
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service approving an access request' do
it 'succeeds' do
- expect { described_class.new(current_user, params).execute(access_requester, **opts) }.to change { source.requesters.count }.by(-1)
+ expect do
+ described_class.new(current_user, params).execute(access_requester, **opts)
+ end.to change { source.requesters.count }.by(-1)
end
it 'returns a <Source>Member' do
@@ -32,7 +36,15 @@ RSpec.describe Members::ApproveAccessRequestService, feature_category: :subgroup
it 'calls the method to resolve access request for the approver' do
expect_next_instance_of(described_class) do |instance|
- expect(instance).to receive(:resolve_access_request_todos).with(current_user, access_requester)
+ expect(instance).to receive(:resolve_access_request_todos).with(access_requester)
+ end
+
+ described_class.new(current_user, params).execute(access_requester, **opts)
+ end
+
+ it 'resolves the todos for the access requests' do
+ expect_next_instance_of(TodoService) do |instance|
+ expect(instance).to receive(:resolve_access_request_todos).with(access_requester)
end
described_class.new(current_user, params).execute(access_requester, **opts)
diff --git a/spec/services/members/base_service_spec.rb b/spec/services/members/base_service_spec.rb
index b2db599db9c..514c25fbc03 100644
--- a/spec/services/members/base_service_spec.rb
+++ b/spec/services/members/base_service_spec.rb
@@ -3,17 +3,16 @@
require 'spec_helper'
RSpec.describe Members::BaseService, feature_category: :projects do
- let_it_be(:current_user) { create(:user) }
let_it_be(:access_requester) { create(:group_member) }
describe '#resolve_access_request_todos' do
it 'calls the resolve_access_request_todos of todo service' do
expect_next_instance_of(TodoService) do |todo_service|
expect(todo_service)
- .to receive(:resolve_access_request_todos).with(current_user, access_requester)
+ .to receive(:resolve_access_request_todos).with(access_requester)
end
- described_class.new.send(:resolve_access_request_todos, current_user, access_requester)
+ described_class.new.send(:resolve_access_request_todos, access_requester)
end
end
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 48f59ba596b..498b9576875 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do
it 'resolves the access request todos for the owner' do
expect_next_instance_of(described_class) do |instance|
- expect(instance).to receive(:resolve_access_request_todos).with(current_user, member)
+ expect(instance).to receive(:resolve_access_request_todos).with(member)
end
described_class.new(current_user).execute(member, **opts)
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 8a0c8f519b0..966782aca98 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -2127,116 +2127,8 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
- context 'relate command' do
- let_it_be_with_refind(:group) { create(:group) }
-
- shared_examples 'relate command' do
- it 'relates issues' do
- service.execute(content, issue)
-
- expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
- end
- end
-
- context 'user is member of group' do
- before do
- group.add_developer(developer)
- end
-
- context 'relate a single issue' do
- let(:other_issue) { create(:issue, project: project) }
- let(:issues_related) { [other_issue] }
- let(:content) { "/relate #{other_issue.to_reference}" }
-
- it_behaves_like 'relate command'
- end
-
- context 'relate multiple issues at once' do
- let(:second_issue) { create(:issue, project: project) }
- let(:third_issue) { create(:issue, project: project) }
- let(:issues_related) { [second_issue, third_issue] }
- let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }
-
- it_behaves_like 'relate command'
- end
-
- context 'when quick action target is unpersisted' do
- let(:issue) { build(:issue, project: project) }
- let(:other_issue) { create(:issue, project: project) }
- let(:issues_related) { [other_issue] }
- let(:content) { "/relate #{other_issue.to_reference}" }
-
- it 'relates the issues after the issue is persisted' do
- service.execute(content, issue)
-
- issue.save!
-
- expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
- end
- end
-
- context 'empty relate command' do
- let(:issues_related) { [] }
- let(:content) { '/relate' }
-
- it_behaves_like 'relate command'
- end
-
- context 'already having related issues' do
- let(:second_issue) { create(:issue, project: project) }
- let(:third_issue) { create(:issue, project: project) }
- let(:issues_related) { [second_issue, third_issue] }
- let(:content) { "/relate #{third_issue.to_reference(project)}" }
-
- before do
- create(:issue_link, source: issue, target: second_issue)
- end
-
- it_behaves_like 'relate command'
- end
-
- context 'cross project' do
- let(:another_group) { create(:group, :public) }
- let(:other_project) { create(:project, group: another_group) }
-
- before do
- another_group.add_developer(developer)
- end
-
- context 'relate a cross project issue' do
- let(:other_issue) { create(:issue, project: other_project) }
- let(:issues_related) { [other_issue] }
- let(:content) { "/relate #{other_issue.to_reference(project)}" }
-
- it_behaves_like 'relate command'
- end
-
- context 'relate multiple cross projects issues at once' do
- let(:second_issue) { create(:issue, project: other_project) }
- let(:third_issue) { create(:issue, project: other_project) }
- let(:issues_related) { [second_issue, third_issue] }
- let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
-
- it_behaves_like 'relate command'
- end
-
- context 'relate a non-existing issue' do
- let(:issues_related) { [] }
- let(:content) { "/relate imaginary##{non_existing_record_iid}" }
-
- it_behaves_like 'relate command'
- end
-
- context 'relate a private issue' do
- let(:private_project) { create(:project, :private) }
- let(:other_issue) { create(:issue, project: private_project) }
- let(:issues_related) { [] }
- let(:content) { "/relate #{other_issue.to_reference(project)}" }
-
- it_behaves_like 'relate command'
- end
- end
- end
+ it_behaves_like 'issues link quick action', :relate do
+ let(:user) { developer }
end
context 'invite_email command' do
@@ -2996,6 +2888,17 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
end
end
end
+
+ describe 'relate command' do
+ let_it_be(:other_issue) { create(:issue, project: project) }
+ let(:content) { "/relate #{other_issue.to_reference}" }
+
+ it 'includes explain message' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(["Marks this issue as related to #{other_issue.to_reference}."])
+ end
+ end
end
describe '#available_commands' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index f0aabd0a8dc..1ec6a3250fc 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -394,6 +394,39 @@ RSpec.describe TodoService, feature_category: :team_planning do
end
end
+ describe '#resolve_todos_with_attributes_for_target' do
+ it 'marks related pending todos to the target for all the users as done' do
+ first_todo = create(:todo, :assigned, user: member, project: project, target: issue, author: author)
+ second_todo = create(:todo, :review_requested, user: john_doe, project: project, target: issue, author: author)
+ another_todo = create(:todo, :assigned, user: john_doe, project: project, target: project, author: author)
+
+ service.resolve_todos_with_attributes_for_target(issue, {})
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_done
+ expect(another_todo.reload).to be_pending
+ end
+
+ it 'marks related only filtered pending todos to the target for all the users as done' do
+ first_todo = create(:todo, :assigned, user: member, project: project, target: issue, author: author)
+ second_todo = create(:todo, :review_requested, user: john_doe, project: project, target: issue, author: author)
+ another_todo = create(:todo, :assigned, user: john_doe, project: project, target: project, author: author)
+
+ service.resolve_todos_with_attributes_for_target(issue, { action: Todo::ASSIGNED })
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).to be_pending
+ expect(another_todo.reload).to be_pending
+ end
+
+ it 'fetches the pending todos with users preloaded' do
+ expect(PendingTodosFinder).to receive(:new)
+ .with(a_hash_including(preload_user_association: true)).and_call_original
+
+ service.resolve_todos_with_attributes_for_target(issue, { action: Todo::ASSIGNED })
+ end
+ end
+
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
@@ -1224,20 +1257,64 @@ RSpec.describe TodoService, feature_category: :team_planning do
end
describe '#resolve_access_request_todos' do
- let_it_be(:source) { create(:group, :public) }
- let_it_be(:requester) { create(:group_member, :access_request, group: source, user: assignee) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:group_requester) { create(:group_member, :access_request, group: group, user: assignee) }
+ let_it_be(:project_requester) { create(:project_member, :access_request, project: project, user: non_member) }
+ let_it_be(:another_pending_todo) { create(:todo, state: :pending, user: john_doe) }
+ # access request by another user
+ let_it_be(:another_group_todo) do
+ create(:todo, state: :pending, target: group, action: Todo::MEMBER_ACCESS_REQUESTED)
+ end
- it 'marks the todos for request handler as done' do
- request_handler_todo = create(:todo,
- user: member,
- state: :pending,
- action: Todo::MEMBER_ACCESS_REQUESTED,
- author: requester.user,
- target: source)
+ let_it_be(:another_project_todo) do
+ create(:todo, state: :pending, target: project, action: Todo::MEMBER_ACCESS_REQUESTED)
+ end
- service.resolve_access_request_todos(member, requester)
+ it 'marks the todos for group access request handlers as done' do
+ access_request_todos = [member, john_doe].map do |group_user|
+ create(:todo,
+ user: group_user,
+ state: :pending,
+ action: Todo::MEMBER_ACCESS_REQUESTED,
+ author: group_requester.user,
+ target: group
+ )
+ end
- expect(request_handler_todo.reload).to be_done
+ expect do
+ service.resolve_access_request_todos(group_requester)
+ end.to change {
+ Todo.pending.where(target: group).for_author(group_requester.user)
+ .for_action(Todo::MEMBER_ACCESS_REQUESTED).count
+ }.from(2).to(0)
+
+ expect(access_request_todos.each(&:reload)).to all be_done
+ expect(another_pending_todo.reload).not_to be_done
+ expect(another_group_todo.reload).not_to be_done
+ end
+
+ it 'marks the todos for project access request handlers as done' do
+ # The project has 1 owner already. Adding another owner here
+ project.add_member(john_doe, Gitlab::Access::OWNER)
+
+ access_request_todo = create(:todo,
+ user: john_doe,
+ state: :pending,
+ action: Todo::MEMBER_ACCESS_REQUESTED,
+ author: project_requester.user,
+ target: project
+ )
+
+ expect do
+ service.resolve_access_request_todos(project_requester)
+ end.to change {
+ Todo.pending.where(target: project).for_author(project_requester.user)
+ .for_action(Todo::MEMBER_ACCESS_REQUESTED).count
+ }.from(2).to(0) # The original owner todo was created with the pending access request
+
+ expect(access_request_todo.reload).to be_done
+ expect(another_pending_todo.reload).to be_pending
+ expect(another_project_todo.reload).to be_pending
end
end
diff --git a/spec/support/capybara_wait_for_all_requests.rb b/spec/support/capybara_wait_for_all_requests.rb
index 86f3e77cc19..36b63619b08 100644
--- a/spec/support/capybara_wait_for_all_requests.rb
+++ b/spec/support/capybara_wait_for_all_requests.rb
@@ -22,7 +22,6 @@ module Capybara
module Node
module Actions
include CapybaraHelpers
- include WaitHelpers
include WaitForRequests
module WaitForAllRequestsAfterClickButton
@@ -33,7 +32,16 @@ module Capybara
end
end
+ module WaitForAllRequestsAfterClickLink
+ def click_link(locator = nil, **options)
+ super
+
+ wait_for_all_requests
+ end
+ end
+
prepend WaitForAllRequestsAfterClickButton
+ prepend WaitForAllRequestsAfterClickLink
end
end
end
diff --git a/spec/support/fast_quarantine.rb b/spec/support/fast_quarantine.rb
index dd805f6ec28..18d4df887a3 100644
--- a/spec/support/fast_quarantine.rb
+++ b/spec/support/fast_quarantine.rb
@@ -1,50 +1,36 @@
# frozen_string_literal: true
-return unless ENV['CI']
return if ENV['FAST_QUARANTINE'] == "false"
return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests')
-require_relative '../../tooling/rspec_flaky/config'
+require_relative '../../tooling/lib/tooling/fast_quarantine'
-# rubocop:disable Style/GlobalVars
RSpec.configure do |config|
- $fast_quarantined_entity_identifiers = begin
- raise "#{ENV['RSPEC_FAST_QUARANTINE_PATH']} doesn't exist" unless File.exist?(ENV['RSPEC_FAST_QUARANTINE_PATH'])
-
- quarantined_entity_identifiers = File.read(ENV['RSPEC_FAST_QUARANTINE_PATH']).lines
- quarantined_entity_identifiers.compact!
- quarantined_entity_identifiers.map! do |quarantined_entity_identifier|
- quarantined_entity_identifier.delete_prefix('./').strip
- end
- rescue => e # rubocop:disable Style/RescueStandardError
- puts e
- []
- end
- $skipped_tests = []
+ fast_quarantine_local_path = ENV.fetch('RSPEC_FAST_QUARANTINE_LOCAL_PATH', 'rspec/fast_quarantine-gitlab.txt')
+ fast_quarantine_path = ENV.fetch(
+ 'RSPEC_FAST_QUARANTINE_PATH',
+ File.expand_path("../../#{fast_quarantine_local_path}", __dir__)
+ )
+ fast_quarantine = Tooling::FastQuarantine.new(fast_quarantine_path: fast_quarantine_path)
+ skipped_examples = []
config.around do |example|
- fast_quarantined_entity_identifier = $fast_quarantined_entity_identifiers.find do |quarantined_entity_identifier|
- case quarantined_entity_identifier
- when /^.+_spec\.rb\[[\d:]+\]$/ # example id, e.g. spec/tasks/gitlab/usage_data_rake_spec.rb[1:5:2:1]
- example.id == "./#{quarantined_entity_identifier}"
- else # whole file, e.g. ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_spec.rb
- example.metadata[:rerun_file_path] == "./#{quarantined_entity_identifier}"
- end
- end
-
- if fast_quarantined_entity_identifier
- puts "Skipping #{example.id} '#{example.full_description}' because it's been fast-quarantined with '#{fast_quarantined_entity_identifier}'."
- $skipped_tests << example.id
+ if fast_quarantine.skip_example?(example)
+ skipped_examples << example.id
+ skip "Skipping #{example.id} because it's been fast-quarantined."
else
example.run
end
end
config.after(:suite) do
- next unless RspecFlaky::Config.skipped_flaky_tests_report_path
- next if $skipped_tests.empty?
+ next if skipped_examples.empty?
+
+ skipped_tests_report_path = ENV.fetch(
+ 'SKIPPED_TESTS_REPORT_PATH',
+ File.expand_path("../../rspec/flaky/skipped_tests.txt", __dir__)
+ )
- File.write(RspecFlaky::Config.skipped_flaky_tests_report_path, "#{ENV['CI_JOB_URL']}\n#{$skipped_tests.join("\n")}\n\n")
+ File.write(skipped_tests_report_path, "#{ENV.fetch('CI_JOB_URL', 'local-run')}\n#{skipped_examples.join("\n")}\n\n")
end
end
-# rubocop:enable Style/GlobalVars
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 921470cdf5c..a451ab27f21 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -973,7 +973,6 @@
- './ee/spec/helpers/ee/subscribable_banner_helper_spec.rb'
- './ee/spec/helpers/ee/system_note_helper_spec.rb'
- './ee/spec/helpers/ee/todos_helper_spec.rb'
-- './ee/spec/helpers/ee/trial_registration_helper_spec.rb'
- './ee/spec/helpers/ee/users/callouts_helper_spec.rb'
- './ee/spec/helpers/ee/version_check_helper_spec.rb'
- './ee/spec/helpers/ee/wiki_helper_spec.rb'
diff --git a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
index 599161abbfe..8f2f3f89914 100644
--- a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
+++ b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
@@ -7,6 +7,10 @@ RSpec.shared_examples 'a metrics embed filter' do
let(:input) { %(<a href="#{url}">example</a>) }
let(:doc) { filter(input) }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
context 'when the document has an external link' do
let(:url) { 'https://foo.com' }
@@ -38,6 +42,18 @@ RSpec.shared_examples 'a metrics embed filter' do
expect(doc.at_css('.js-render-metrics')).to be_present
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'does not append a metrics chart placeholder' do
+ node = doc.at_css('.js-render-metrics')
+
+ expect(node).not_to be_present
+ end
+ end
end
# Nokogiri escapes the URLs, but we don't care about that
diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
index 3faa8f9c032..7ace223723c 100644
--- a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
+++ b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-RSpec.shared_examples 'delegates AI request to Workhorse' do
- context 'when openai_experimentation is disabled' do
+RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag|
+ context "when #{provider_flag} is disabled" do
before do
- stub_feature_flags(openai_experimentation: false)
+ stub_feature_flags(provider_flag => false)
end
it 'responds as not found' do
diff --git a/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb
new file mode 100644
index 00000000000..811b5ee4de2
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/issue_links_quick_actions_shared_examples.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issues link quick action' do |command|
+ let_it_be_with_refind(:group) { create(:group) }
+ let_it_be_with_reload(:other_issue) { create(:issue, project: project) }
+ let_it_be_with_reload(:second_issue) { create(:issue, project: project) }
+ let_it_be_with_reload(:third_issue) { create(:issue, project: project) }
+
+ let(:link_type) { command == :relate ? 'relates_to' : 'blocks' }
+ let(:links_query) do
+ if command == :blocked_by
+ IssueLink.where(target: issue, link_type: link_type).map(&:source)
+ else
+ IssueLink.where(source: issue, link_type: link_type).map(&:target)
+ end
+ end
+
+ shared_examples 'link command' do
+ it 'links issues' do
+ service.execute(content, issue)
+
+ expect(links_query).to match_array(issues_linked)
+ end
+ end
+
+ context 'when user is member of group' do
+ before do
+ group.add_developer(user)
+ end
+
+ context 'when linking a single issue' do
+ let(:issues_linked) { [other_issue] }
+ let(:content) { "/#{command} #{other_issue.to_reference}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking multiple issues at once' do
+ let(:issues_linked) { [second_issue, third_issue] }
+ let(:content) { "/#{command} #{second_issue.to_reference} #{third_issue.to_reference}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when quick action target is unpersisted' do
+ let(:issue) { build(:issue, project: project) }
+ let(:issues_linked) { [other_issue] }
+ let(:content) { "/#{command} #{other_issue.to_reference}" }
+
+ it 'links the issues after the issue is persisted' do
+ service.execute(content, issue)
+
+ issue.save!
+
+ expect(links_query).to match_array(issues_linked)
+ end
+ end
+
+ context 'with empty link command' do
+ let(:issues_linked) { [] }
+ let(:content) { "/#{command}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'with already having linked issues' do
+ let(:issues_linked) { [second_issue, third_issue] }
+ let(:content) { "/#{command} #{third_issue.to_reference(project)}" }
+
+ before do
+ create_existing_link(command)
+ end
+
+ it_behaves_like 'link command'
+ end
+
+ context 'with cross project' do
+ let_it_be_with_reload(:another_group) { create(:group, :public) }
+ let_it_be_with_reload(:other_project) { create(:project, group: another_group) }
+
+ before do
+ another_group.add_developer(user)
+ [other_issue, second_issue, third_issue].map { |i| i.update!(project: other_project) }
+ end
+
+ context 'when linking a cross project issue' do
+ let(:issues_linked) { [other_issue] }
+ let(:content) { "/#{command} #{other_issue.to_reference(project)}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking multiple cross projects issues at once' do
+ let(:issues_linked) { [second_issue, third_issue] }
+ let(:content) { "/#{command} #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking a non-existing issue' do
+ let(:issues_linked) { [] }
+ let(:content) { "/#{command} imaginary##{non_existing_record_iid}" }
+
+ it_behaves_like 'link command'
+ end
+
+ context 'when linking a private issue' do
+ let_it_be(:private_issue) { create(:issue, project: create(:project, :private)) }
+ let(:issues_linked) { [] }
+ let(:content) { "/#{command} #{private_issue.to_reference(project)}" }
+
+ it_behaves_like 'link command'
+ end
+ end
+ end
+
+ def create_existing_link(command)
+ issues = [issue, second_issue]
+ source, target = command == :blocked_by ? issues.reverse : issues
+
+ create(:issue_link, source: source, target: target, link_type: link_type)
+ end
+end
diff --git a/spec/support_specs/capybara_wait_for_all_requests_spec.rb b/spec/support_specs/capybara_wait_for_all_requests_spec.rb
index fd105c3ab01..ddd4be6c644 100644
--- a/spec/support_specs/capybara_wait_for_all_requests_spec.rb
+++ b/spec/support_specs/capybara_wait_for_all_requests_spec.rb
@@ -34,10 +34,28 @@ RSpec.describe 'capybara_wait_for_all_requests', feature_category: :tooling do #
end.new
end
- it 'waits for all requests after a page visit' do
+ it 'waits for all requests after a click button' do
expect(node).to receive(:wait_for_all_requests)
node.click_button
end
end
+
+ context 'for Capybara::Node::Actions::WaitForAllRequestsAfterClickLink' do
+ let(:node) do
+ Class.new do
+ def click_link(locator = nil, **_options)
+ locator
+ end
+
+ prepend Capybara::Node::Actions::WaitForAllRequestsAfterClickLink
+ end.new
+ end
+
+ it 'waits for all requests after a click link' do
+ expect(node).to receive(:wait_for_all_requests)
+
+ node.click_link
+ end
+ end
end
diff --git a/spec/tooling/lib/tooling/fast_quarantine_spec.rb b/spec/tooling/lib/tooling/fast_quarantine_spec.rb
new file mode 100644
index 00000000000..bb60a335ce2
--- /dev/null
+++ b/spec/tooling/lib/tooling/fast_quarantine_spec.rb
@@ -0,0 +1,193 @@
+# frozen_string_literal: true
+
+require_relative '../../../../tooling/lib/tooling/fast_quarantine'
+require 'tempfile'
+
+RSpec.describe Tooling::FastQuarantine, feature_category: :tooling do
+ attr_accessor :fast_quarantine_file
+
+ around do |example|
+ self.fast_quarantine_file = Tempfile.new('fast_quarantine_file')
+
+ # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
+ # Tempfile.html#class-Tempfile-label-Explicit+close
+ begin
+ example.run
+ ensure
+ fast_quarantine_file.close
+ fast_quarantine_file.unlink
+ end
+ end
+
+ let(:fast_quarantine_path) { fast_quarantine_file.path }
+ let(:fast_quarantine_file_content) { '' }
+ let(:instance) do
+ described_class.new(fast_quarantine_path: fast_quarantine_path)
+ end
+
+ before do
+ File.write(fast_quarantine_path, fast_quarantine_file_content)
+ end
+
+ describe '#initialize' do
+ context 'when fast_quarantine_path does not exist' do
+ it 'prints a warning' do
+ allow(File).to receive(:exist?).and_return(false)
+
+ expect { instance }.to output("#{fast_quarantine_path} doesn't exist!\n").to_stderr
+ end
+ end
+
+ context 'when fast_quarantine_path exists' do
+ it 'does not raise an error' do
+ expect { instance }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#identifiers' do
+ before do
+ allow(File).to receive(:read).and_call_original
+ end
+
+ context 'when the fast quarantine file is empty' do
+ let(:fast_quarantine_file_content) { '' }
+
+ it 'returns []' do
+ expect(instance.identifiers).to eq([])
+ end
+ end
+
+ context 'when the fast quarantine file is not empty' do
+ let(:fast_quarantine_file_content) { "./spec/foo_spec.rb\nspec/foo_spec.rb:42\n./spec/baz_spec.rb[1:2:3]" }
+
+ it 'returns parsed and sanitized lines' do
+ expect(instance.identifiers).to eq(%w[
+ spec/foo_spec.rb
+ spec/foo_spec.rb:42
+ spec/baz_spec.rb[1:2:3]
+ ])
+ end
+
+ context 'when reading the file raises an error' do
+ before do
+ allow(File).to receive(:read).with(fast_quarantine_path).and_raise('')
+ end
+
+ it 'returns []' do
+ expect(instance.identifiers).to eq([])
+ end
+ end
+
+ describe 'memoization' do
+ it 'memoizes the identifiers list' do
+ expect(File).to receive(:read).with(fast_quarantine_path).once.and_call_original
+
+ instance.identifiers
+
+ # calling #identifiers again doesn't call File.read
+ instance.identifiers
+ end
+ end
+ end
+ end
+
+ describe '#skip_example?' do
+ let(:fast_quarantine_file_content) { "./spec/foo_spec.rb\nspec/bar_spec.rb:42\n./spec/baz_spec.rb[1:2:3]" }
+ let(:example_id) { './spec/foo_spec.rb[1:2:3]' }
+ let(:example_metadata) { {} }
+ let(:example) { instance_double(RSpec::Core::Example, id: example_id, metadata: example_metadata) }
+
+ describe 'skipping example by id' do
+ let(:example_id) { './spec/baz_spec.rb[1:2:3]' }
+
+ it 'skips example by id' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+
+ describe 'skipping example by line' do
+ context 'when example location matches' do
+ let(:example_metadata) do
+ { location: './spec/bar_spec.rb:42' }
+ end
+
+ it 'skips example by line' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+
+ context 'when example group location matches' do
+ let(:example_metadata) do
+ {
+ example_group: { location: './spec/bar_spec.rb:42' }
+ }
+ end
+
+ it 'skips example by line' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+
+ context 'when nested parent example group location matches' do
+ let(:example_metadata) do
+ {
+ example_group: {
+ parent_example_group: {
+ parent_example_group: {
+ parent_example_group: { location: './spec/bar_spec.rb:42' }
+ }
+ }
+ }
+ }
+ end
+
+ it 'skips example by line' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+ end
+
+ describe 'skipping example by file' do
+ context 'when example file_path matches' do
+ let(:example_metadata) do
+ { file_path: './spec/foo_spec.rb' }
+ end
+
+ it 'skips example by file' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+
+ context 'when example group file_path matches' do
+ let(:example_metadata) do
+ {
+ example_group: { file_path: './spec/foo_spec.rb' }
+ }
+ end
+
+ it 'skips example by file' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+
+ context 'when nested parent example group file_path matches' do
+ let(:example_metadata) do
+ {
+ example_group: {
+ parent_example_group: {
+ parent_example_group: {
+ parent_example_group: { file_path: './spec/foo_spec.rb' }
+ }
+ }
+ }
+ }
+ end
+
+ it 'skips example by file' do
+ expect(instance.skip_example?(example)).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/tooling/rspec_flaky/config_spec.rb b/spec/tooling/rspec_flaky/config_spec.rb
index c95e5475d66..63f42d7c6cc 100644
--- a/spec/tooling/rspec_flaky/config_spec.rb
+++ b/spec/tooling/rspec_flaky/config_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
stub_env('FLAKY_RSPEC_SUITE_REPORT_PATH', nil)
stub_env('FLAKY_RSPEC_REPORT_PATH', nil)
stub_env('NEW_FLAKY_RSPEC_REPORT_PATH', nil)
- stub_env('SKIPPED_FLAKY_TESTS_REPORT_PATH', nil)
# Ensure the behavior is the same locally and on CI (where Rails is defined since we run this test as part of the whole suite), i.e. Rails isn't defined
allow(described_class).to receive(:rails_path).and_wrap_original do |method, path|
path
@@ -104,22 +103,4 @@ RSpec.describe RspecFlaky::Config, :aggregate_failures do
end
end
end
-
- describe '.skipped_flaky_tests_report_path' do
- context "when ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] is not set" do
- it 'returns the default path' do
- expect(described_class.skipped_flaky_tests_report_path).to eq('rspec/flaky/skipped_flaky_tests_report.txt')
- end
- end
-
- context "when ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] is set" do
- before do
- stub_env('SKIPPED_FLAKY_TESTS_REPORT_PATH', 'foo/skipped_flaky_tests_report.txt')
- end
-
- it 'returns the value of the env variable' do
- expect(described_class.skipped_flaky_tests_report_path).to eq('foo/skipped_flaky_tests_report.txt')
- end
- end
- end
end
diff --git a/spec/workers/ssh_keys/update_last_used_at_worker_spec.rb b/spec/workers/ssh_keys/update_last_used_at_worker_spec.rb
new file mode 100644
index 00000000000..33b3b44955d
--- /dev/null
+++ b/spec/workers/ssh_keys/update_last_used_at_worker_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SshKeys::UpdateLastUsedAtWorker, type: :worker, feature_category: :source_code_management do
+ let_it_be(:key) { create(:key) }
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [key.id] }
+ end
+
+ describe '#perform' do
+ subject(:worker) { described_class.new }
+
+ it 'updates last_used_at column', :freeze_time do
+ expect { worker.perform(key.id) }.to change { key.reload.last_used_at }.to(Time.zone.now)
+ end
+
+ it 'does not update updated_at column' do
+ expect { worker.perform(key.id) }.not_to change { key.reload.updated_at }
+ end
+ end
+end
diff --git a/tests.yml b/tests.yml
index e4c84789459..b2d8311fb7e 100644
--- a/tests.yml
+++ b/tests.yml
@@ -121,3 +121,9 @@ mapping:
# See https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/1360
- source: vendor/project_templates/.*
test: spec/lib/gitlab/project_template_spec.rb
+
+ # See https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/1683#note_1385966977
+ - source: app/finders/members_finder.rb
+ test: spec/graphql/types/project_member_relation_enum_spec.rb
+ - source: app/finders/group_members_finder.rb
+ test: spec/graphql/types/group_member_relation_enum_spec.rb
diff --git a/tooling/lib/tooling/fast_quarantine.rb b/tooling/lib/tooling/fast_quarantine.rb
new file mode 100644
index 00000000000..a0dc8bc460b
--- /dev/null
+++ b/tooling/lib/tooling/fast_quarantine.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Tooling
+ class FastQuarantine
+ def initialize(fast_quarantine_path:)
+ warn "#{fast_quarantine_path} doesn't exist!" unless File.exist?(fast_quarantine_path.to_s)
+
+ @fast_quarantine_path = fast_quarantine_path
+ end
+
+ def identifiers
+ @identifiers ||= begin
+ quarantined_entity_identifiers = File.read(fast_quarantine_path).lines
+ quarantined_entity_identifiers.compact!
+ quarantined_entity_identifiers.map! do |quarantined_entity_identifier|
+ quarantined_entity_identifier.delete_prefix('./').strip
+ end
+ rescue => e # rubocop:disable Style/RescueStandardError
+ $stdout.puts e
+ []
+ end
+ end
+
+ def skip_example?(example)
+ identifiers.find do |quarantined_entity_identifier|
+ case quarantined_entity_identifier
+ when /^.+_spec\.rb\[[\d:]+\]$/ # example id, e.g. spec/tasks/gitlab/usage_data_rake_spec.rb[1:5:2:1]
+ example.id == "./#{quarantined_entity_identifier}"
+ when /^.+_spec\.rb:\d+$/ # file + line, e.g. spec/tasks/gitlab/usage_data_rake_spec.rb:42
+ fetch_metadata_from_ancestors(example, :location)
+ .any?("./#{quarantined_entity_identifier}")
+ when /^.+_spec\.rb$/ # whole file, e.g. ee/spec/features/boards/swimlanes/epics_swimlanes_sidebar_spec.rb
+ fetch_metadata_from_ancestors(example, :file_path)
+ .any?("./#{quarantined_entity_identifier}")
+ end
+ end
+ end
+
+ private
+
+ attr_reader :fast_quarantine_path
+
+ def fetch_metadata_from_ancestors(example, attribute)
+ metadata = [example.metadata[attribute]]
+ example_group = example.metadata[:example_group]
+
+ loop do
+ break if example_group.nil?
+
+ metadata << example_group[attribute]
+ example_group = example_group[:parent_example_group]
+ end
+
+ metadata
+ end
+ end
+end
diff --git a/tooling/rspec_flaky/config.rb b/tooling/rspec_flaky/config.rb
index 36e35671587..0e36e985aad 100644
--- a/tooling/rspec_flaky/config.rb
+++ b/tooling/rspec_flaky/config.rb
@@ -18,10 +18,6 @@ module RspecFlaky
ENV['NEW_FLAKY_RSPEC_REPORT_PATH'] || rails_path("rspec/flaky/new-report.json")
end
- def self.skipped_flaky_tests_report_path
- ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'] || rails_path("rspec/flaky/skipped_flaky_tests_report.txt")
- end
-
def self.rails_path(path)
return path unless defined?(Rails)