Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-30 18:07:31 +00:00
parent 7e7619d2c9
commit c07666f925
73 changed files with 1182 additions and 463 deletions

View File

@ -83,7 +83,6 @@ Database/AvoidUsingPluckWithoutLimit:
- 'app/services/merge_requests/pushed_branches_service.rb'
- 'app/services/projects/unlink_fork_service.rb'
- 'ee/app/finders/ee/issuables/label_filter.rb'
- 'ee/app/finders/ee/merge_requests_finder.rb'
- 'ee/app/finders/groups_with_templates_finder.rb'
- 'ee/app/finders/namespaces/billed_users_finder.rb'
- 'ee/app/finders/namespaces/free_user_cap/users_without_added_members_finder.rb'

View File

@ -8,7 +8,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'app/controllers/projects/settings/integrations_controller.rb'
- 'app/controllers/repositories/git_http_controller.rb'
- 'app/finders/abuse_reports_finder.rb'
- 'app/finders/merge_requests_finder.rb'
- 'app/graphql/types/namespace_type.rb'
- 'app/graphql/types/project_type.rb'
- 'app/helpers/auto_devops_helper.rb'
@ -65,7 +64,6 @@ Gitlab/FeatureFlagWithoutActor:
- 'ee/app/controllers/ee/admin/application_settings_controller.rb'
- 'ee/app/controllers/ee/omniauth_callbacks_controller.rb'
- 'ee/app/controllers/groups/billings_controller.rb'
- 'ee/app/finders/ee/merge_requests_finder.rb'
- 'ee/app/graphql/resolvers/ai/code_suggestions_access_resolver.rb'
- 'ee/app/graphql/types/epic_type.rb'
- 'ee/app/helpers/billing_plans_helper.rb'

View File

@ -38,9 +38,7 @@ Gitlab/NoFindInWorkers:
- 'app/workers/issuable_export_csv_worker.rb'
- 'app/workers/issues/placement_worker.rb'
- 'app/workers/members_destroyer/unassign_issuables_worker.rb'
- 'app/workers/merge_requests/delete_source_branch_worker.rb'
- 'app/workers/merge_requests/handle_assignees_change_worker.rb'
- 'app/workers/merge_requests/resolve_todos_worker.rb'
- 'app/workers/merge_worker.rb'
- 'app/workers/namespaces/root_statistics_worker.rb'
- 'app/workers/namespaces/schedule_aggregation_worker.rb'

View File

@ -856,7 +856,6 @@ Layout/LineLength:
- 'ee/lib/gitlab/ci/reports/security/locations/cluster_image_scanning.rb'
- 'ee/lib/gitlab/contribution_analytics/data_collector.rb'
- 'ee/lib/gitlab/elastic/group_search_results.rb'
- 'ee/lib/gitlab/elastic/project_search_results.rb'
- 'ee/lib/gitlab/expiring_subscription_message.rb'
- 'ee/lib/gitlab/geo.rb'
- 'ee/lib/gitlab/geo/git_ssh_proxy.rb'
@ -2117,7 +2116,6 @@ Layout/LineLength:
- 'lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb'
- 'lib/gitlab/grape_logging/loggers/client_env_logger.rb'
- 'lib/gitlab/graphql/timeout.rb'
- 'lib/gitlab/group_search_results.rb'
- 'lib/gitlab/hook_data/key_builder.rb'
- 'lib/gitlab/hotlinking_detector.rb'
- 'lib/gitlab/http_io.rb'

View File

@ -1,116 +0,0 @@
---
Migration/PreventEnablingLockRetriesForTransactionalMigrations:
Exclude:
- 'db/migrate/20240221145450_create_audit_events_instance_streaming_event_type_filters.rb'
- 'db/migrate/20240304184128_create_ci_build_names_table.rb'
- 'db/migrate/20240304195555_add_search_vector_to_p_ci_build_names.rb'
- 'db/migrate/20240304195852_create_partitions_for_p_ci_build_names.rb'
- 'db/migrate/20240305201830_add_custom_headers_to_web_hook.rb'
- 'db/migrate/20240320060913_add_container_scanning_for_registry_toggle_to_security_project_settings.rb'
- 'db/migrate/20240320062459_add_trial_to_subscription_add_on_purchases.rb'
- 'db/migrate/20240325115147_create_project_saved_replies_table.rb'
- 'db/migrate/20240325150539_add_pre_receive_secret_detection_enabled_to_project_security_settings.rb'
- 'db/migrate/20240326144116_add_zoekt_settings_to_application_settings.rb'
- 'db/migrate/20240327114933_add_override_changes_requested_to_merge_request.rb'
- 'db/migrate/20240402181020_create_audit_events_streaming_instance_namespace_filters.rb'
- 'db/migrate/20240403000000_add_fallback_behavior_to_scan_result_policy_reads.rb'
- 'db/migrate/20240404191440_add_early_access_program_participant_to_user_preferences.rb'
- 'db/migrate/20240404192955_create_early_access_program_tracking_events.rb'
- 'db/migrate/20240408105626_add_send_bot_message_to_policies.rb'
- 'db/migrate/20240409013009_add_importers_to_application_settings.rb'
- 'db/migrate/20240418135657_add_tickets_confidential_by_default_to_service_desk_settings.rb'
- 'db/migrate/20240419071412_create_audit_events_streaming_group_namespace_filters.rb'
- 'db/migrate/20240419124207_add_runner_owner_namespace_id_column_to_ci_running_builds.rb'
- 'db/migrate/20240429182325_create_custom_software_licenses_table.rb'
- 'db/migrate/20240503173159_create_user_audit_events.rb'
- 'db/migrate/20240503174905_create_group_audit_events.rb'
- 'db/migrate/20240503175347_create_project_audit_events.rb'
- 'db/migrate/20240505153633_create_instance_audit_events.rb'
- 'db/migrate/20240519141301_add_metadata_to_member_approvals.rb'
- 'db/migrate/20240528142856_add_organization_id_to_subscription_add_on_purchases.rb'
- 'db/migrate/20240604051641_create_partitions_for_p_ci_build_sources.rb'
- 'db/migrate/20240604081941_add_approval_policy_rule_id_to_approval_group_rules.rb'
- 'db/migrate/20240604082005_add_approval_policy_rule_id_to_approval_project_rules.rb'
- 'db/migrate/20240604082023_add_approval_policy_rule_id_to_approval_merge_request_rules.rb'
- 'db/migrate/20240604082113_add_approval_policy_rule_id_to_software_license_policies.rb'
- 'db/migrate/20240604082344_add_approval_policy_rule_id_to_scan_result_policy_violations.rb'
- 'db/migrate/20240606124806_add_organization_id_to_snippets.rb'
- 'db/migrate/20240607035355_add_member_role_id_to_ldap_group_links.rb'
- 'db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb'
- 'db/migrate/20240625050115_add_member_role_id_to_group_group_links.rb'
- 'db/migrate/20240627055916_add_uploaded_by_user_id_to_uploads.rb'
- 'db/migrate/20240628203616_update_scheduled_scans_max_concurrency_in_application_settings_for_self_managed.rb'
- 'db/migrate/20240701153843_add_work_items_dates_sources_sync_to_issues_trigger.rb'
- 'db/migrate/20240703043908_create_table_p_ci_build_tags.rb'
- 'db/migrate/20240703054001_ensure_unique_id_for_p_ci_build_tags.rb'
- 'db/migrate/20240703082453_create_partitions_for_p_ci_build_tags.rb'
- 'db/migrate/20240705020837_add_last_access_from_pipl_country_at_to_users.rb'
- 'db/migrate/20240717140800_add_encrypted_shared_secret_to_external_status_checks.rb'
- 'db/migrate/20240719090901_add_plan_id_to_ci_pending_builds.rb'
- 'db/migrate/20240719090902_add_allowed_plan_ids_to_ci_runners.rb'
- 'db/migrate/20240725154651_add_project_id_to_packages_dependencies.rb'
- 'db/migrate/20240813095256_add_disable_password_authentication_for_enterprise_users_to_saml_providers.rb'
- 'db/migrate/20240813170304_allow_null_for_upstream_id_in_virtual_registries_packages_maven_cached_responses.rb'
- 'db/migrate/20240816040224_create_import_member_placeholder_references.rb'
- 'db/migrate/20240816061320_allow_top_level_group_owners_to_create_service_accounts.rb'
- 'db/migrate/20240819104531_add_observability_backend_ssl_verification_enabled_to_application_settings.rb'
- 'db/migrate/20240828103148_add_spp_repository_pipeline_access_to_project_settings.rb'
- 'db/migrate/20240829125828_change_time_estimate_default_from_null_to_zero_on_issues.rb'
- 'db/migrate/20240829125928_change_time_estimate_default_from_null_to_zero_on_merge_requests.rb'
- 'db/migrate/20240904133620_add_spp_repository_pipeline_access_cascading_setting.rb'
- 'db/migrate/20240905124106_add_policy_action_limit_application_setting.rb'
- 'db/migrate/20240917054235_create_wiki_page_user_mentions.rb'
- 'db/migrate/20240923130542_add_sharding_key_id_to_ci_runners.rb'
- 'db/migrate/20241008101731_create_catalog_resource_component_last_usages_table.rb'
- 'db/migrate/20241016125024_add_metadata_to_zoekt_indices.rb'
- 'db/migrate/20241022141656_add_policy_tuning_to_policies.rb'
- 'db/migrate/20241030031829_add_resource_usage_limits_to_application_settings.rb'
- 'db/migrate/20241104224549_add_allow_list_integrations_settings_to_application_settings.rb'
- 'db/migrate/20241107131541_add_user_seat_management_to_application_settings.rb'
- 'db/migrate/20241112123436_update_seat_control_in_application_settings.rb'
- 'db/migrate/20241115075017_add_member_role_id_to_project_group_links.rb'
- 'db/migrate/20241122121328_add_approval_policy_action_idx_to_approval_project_rules.rb'
- 'db/migrate/20241122121350_add_approval_policy_action_idx_to_approval_merge_request_rules.rb'
- 'db/migrate/20241122121652_add_action_idx_to_scan_result_policies.rb'
- 'db/migrate/20241201162318_add_custom_roles_to_scan_result_policies.rb'
- 'db/migrate/20241202054640_add_vulnerability_events_to_web_hooks.rb'
- 'db/migrate/20250108062227_add_extended_grat_expiry_webhook_execute_to_namespace_settings.rb'
- 'db/migrate/20250108062256_add_extended_prat_expiry_webhook_execute_to_project_settings.rb'
- 'db/migrate/20250109102301_add_o11y_settings_to_application_settings.rb'
- 'db/migrate/20250210225045_add_vscode_extension_marketplace_to_application_setting.rb'
- 'db/migrate/20250227095537_add_organization_id_to_fork_networks.rb'
- 'db/migrate/20250313142550_add_job_token_policies_enabled_column_to_namespace_settings.rb'
- 'db/migrate/20250320072111_add_security_policies_namespace_setting.rb'
- 'db/migrate/20250320103054_add_organization_id_to_subscription_seat_assignments.rb'
- 'db/migrate/20250325071830_add_member_approval_events_to_web_hooks.rb'
- 'db/post_migrate/20240205170838_change_approval_merge_request_rules_vulnerability_states_default.rb'
- 'db/post_migrate/20240205171942_change_approval_project_rules_vulnerability_states_default.rb'
- 'db/post_migrate/20240318180554_drop_promote_ultimate_features_at_column.rb'
- 'db/post_migrate/20240408135652_drop_external_approval_rules_protected_branches_table.rb'
- 'db/post_migrate/20240513042657_cleanup_bigint_conversions_for_ci_pipelines.rb'
- 'db/post_migrate/20240513065051_ensure_id_uniqueness_for_p_ci_builds_execution_configs.rb'
- 'db/post_migrate/20240528115140_change_projects_organization_id_default.rb'
- 'db/post_migrate/20240627165253_drop_token_with_ivs_table.rb'
- 'db/post_migrate/20240709014310_cleanup_bigint_conversions_for_p_ci_builds_attempt2.rb'
- 'db/post_migrate/20240715055251_cleanup_bigint_conversion_for_merge_requests_head_pipeline_id.rb'
- 'db/post_migrate/20240715055311_cleanup_bigint_conversion_for_merge_request_metrics_pipeline_id.rb'
- 'db/post_migrate/20240715055356_cleanup_bigint_conversion_for_merge_trains_pipeline_id.rb'
- 'db/post_migrate/20240715055415_cleanup_bigint_conversion_for_vulnerability_feedback_pipeline_id.rb'
- 'db/post_migrate/20240715055432_cleanup_bigint_conversion_for_vulnerability_occurrence_pipelines_pipeline_id.rb'
- 'db/post_migrate/20240719090903_track_plan_deletions.rb'
- 'db/post_migrate/20240807063339_remove_application_settings_required_instance_ci_template_column.rb'
- 'db/post_migrate/20240814052751_cleanup_bigint_conversion_for_packages_build_infos_pipeline_id.rb'
- 'db/post_migrate/20240903062554_change_raw_usage_data_org_default.rb'
- 'db/post_migrate/20240920131119_drop_p_ci_finished_build_ch_sync_event_project_id_default.rb'
- 'db/post_migrate/20250204181430_cleanup_bigint_conversion_for_geo_event_log_geo_event_id.rb'
- 'db/post_migrate/20250311113123_ensure_id_uniqueness_for_p_ci_runners.rb'
- 'db/post_migrate/20250311113127_ensure_id_uniqueness_for_p_ci_runner_machines.rb'
- 'ee/db/embedding/migrate/20230420103900_create_tanuki_bot_mvc.rb'
- 'ee/db/embedding/migrate/20230501095300_add_version_to_tanuki_bot_mvc.rb'
- 'ee/db/embedding/migrate/20230821103900_create_vertex_gitlab_docs.rb'
- 'ee/db/embedding/post_migrate/20231030093125_drop_tanuki_bot_mvc_table.rb'
- 'ee/db/geo/post_migrate/20240214235202_remove_snippet_repository_registry_force_to_redownload_column.rb'
- 'ee/db/geo/post_migrate/20240214235239_remove_project_wiki_repository_registry_force_to_redownload_column.rb'
- 'ee/db/geo/post_migrate/20240214235323_remove_project_repository_registry_force_to_redownload_column.rb'
- 'ee/db/geo/post_migrate/20240214235349_remove_group_wiki_repository_registry_force_to_redownload_column.rb'
- 'ee/db/geo/post_migrate/20240214235418_remove_design_management_repository_registry_force_to_redownload_column.rb'

View File

@ -2,7 +2,6 @@
# Cop supports --autocorrect.
RSpec/AnyInstanceOf:
Exclude:
- 'ee/spec/features/issues/new/form_spec.rb'
- 'ee/spec/features/projects/new_project_spec.rb'
- 'ee/spec/features/security/project/internal_access_spec.rb'
- 'ee/spec/features/security/project/private_access_spec.rb'

View File

@ -146,7 +146,6 @@ RSpec/BeEq:
- 'ee/spec/lib/gitlab/insights/finders/projects_finder_spec.rb'
- 'ee/spec/lib/gitlab/insights/project_insights_config_spec.rb'
- 'ee/spec/lib/gitlab/licenses/submit_license_usage_data_banner_spec.rb'
- 'ee/spec/lib/gitlab/llm/ai_gateway/docs_client_spec.rb'
- 'ee/spec/lib/gitlab/llm/chain/answer_spec.rb'
- 'ee/spec/lib/gitlab/llm/chain/requests/ai_gateway_spec.rb'
- 'ee/spec/lib/gitlab/llm/chain/tools/gitlab_documentation/executor_spec.rb'

View File

@ -146,7 +146,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/finders/incident_management/oncall_schedules_finder_spec.rb'
- 'ee/spec/finders/iterations/cadences_finder_spec.rb'
- 'ee/spec/finders/iterations_finder_spec.rb'
- 'ee/spec/finders/merge_requests_finder_spec.rb'
- 'ee/spec/finders/snippets_finder_spec.rb'
- 'ee/spec/frontend/fixtures/analytics/charts.rb'
- 'ee/spec/frontend/fixtures/analytics/contributions_spec.rb'

View File

@ -2708,7 +2708,6 @@ RSpec/ContextWording:
- 'spec/tasks/gitlab/dependency_proxy/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/gitaly_rake_spec.rb'
- 'spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/packages/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/terraform/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/workhorse_rake_spec.rb'
- 'spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb'

View File

@ -1960,7 +1960,6 @@ RSpec/FeatureCategory:
- 'spec/lib/gitlab/ci/artifacts/logger_spec.rb'
- 'spec/lib/gitlab/ci/artifacts/metrics_spec.rb'
- 'spec/lib/gitlab/ci/badge/coverage/metadata_spec.rb'
- 'spec/lib/gitlab/ci/badge/coverage/report_spec.rb'
- 'spec/lib/gitlab/ci/badge/coverage/template_spec.rb'
- 'spec/lib/gitlab/ci/badge/pipeline/metadata_spec.rb'
- 'spec/lib/gitlab/ci/badge/pipeline/template_spec.rb'
@ -3586,7 +3585,6 @@ RSpec/FeatureCategory:
- 'spec/serializers/fork_namespace_serializer_spec.rb'
- 'spec/serializers/group_access_token_entity_spec.rb'
- 'spec/serializers/group_access_token_serializer_spec.rb'
- 'spec/serializers/group_child_entity_spec.rb'
- 'spec/serializers/group_child_serializer_spec.rb'
- 'spec/serializers/group_deploy_key_entity_spec.rb'
- 'spec/serializers/group_link/group_group_link_serializer_spec.rb'
@ -3689,7 +3687,6 @@ RSpec/FeatureCategory:
- 'spec/tasks/gitlab/ldap_rake_spec.rb'
- 'spec/tasks/gitlab/lfs/check_rake_spec.rb'
- 'spec/tasks/gitlab/lfs/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/packages/migrate_rake_spec.rb'
- 'spec/tasks/gitlab/pages_rake_spec.rb'
- 'spec/tasks/gitlab/password_rake_spec.rb'
- 'spec/tasks/gitlab/praefect_rake_spec.rb'

View File

@ -188,7 +188,6 @@ Sidekiq/EnforceDatabaseHealthSignalDeferral:
- 'ee/app/workers/onboarding/progress_tracking_worker.rb'
- 'ee/app/workers/package_metadata/advisories_sync_worker.rb'
- 'ee/app/workers/package_metadata/cve_enrichment_sync_worker.rb'
- 'ee/app/workers/package_metadata/global_advisory_scan_worker.rb'
- 'ee/app/workers/package_metadata/licenses_sync_worker.rb'
- 'ee/app/workers/projects/deregister_suggested_reviewers_project_worker.rb'
- 'ee/app/workers/projects/disable_legacy_open_source_license_for_inactive_projects_worker.rb'

View File

@ -182,7 +182,6 @@ Style/FormatString:
- 'spec/controllers/graphql_controller_spec.rb'
- 'spec/factories/lfs_objects.rb'
- 'spec/features/admin/admin_users_spec.rb'
- 'spec/features/groups/import_export/connect_instance_spec.rb'
- 'spec/features/issues/new/form_spec.rb'
- 'spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb'
- 'spec/helpers/profiles_helper_spec.rb'

View File

@ -1014,7 +1014,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/finders/ee/fork_targets_finder.rb'
- 'ee/app/finders/ee/issuables/label_filter.rb'
- 'ee/app/finders/ee/issues_finder.rb'
- 'ee/app/finders/ee/merge_requests_finder.rb'
- 'ee/app/finders/ee/notes_finder.rb'
- 'ee/app/finders/epics/with_issues_finder.rb'
- 'ee/app/finders/geo/framework_registry_finder.rb'
@ -1219,7 +1218,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/services/ee/groups/autocomplete_service.rb'
- 'ee/app/services/ee/groups/destroy_service.rb'
- 'ee/app/services/ee/groups/update_service.rb'
- 'ee/app/services/ee/keys/create_service.rb'
- 'ee/app/services/ee/labels/promote_service.rb'
- 'ee/app/services/ee/members/create_service.rb'
- 'ee/app/services/ee/members/creator_service.rb'
@ -1235,7 +1233,6 @@ Style/InlineDisableAnnotation:
- 'ee/app/services/ee/quick_actions/target_service.rb'
- 'ee/app/services/ee/resource_events/synthetic_iteration_notes_builder_service.rb'
- 'ee/app/services/ee/resource_events/synthetic_weight_notes_builder_service.rb'
- 'ee/app/services/ee/search_service.rb'
- 'ee/app/services/ee/system_note_service.rb'
- 'ee/app/services/ee/users/build_service.rb'
- 'ee/app/services/ee/users/destroy_service.rb'
@ -1462,7 +1459,6 @@ Style/InlineDisableAnnotation:
- 'ee/lib/gitlab/contribution_analytics/postgresql_data_collector.rb'
- 'ee/lib/gitlab/cube_js/data_transformer.rb'
- 'ee/lib/gitlab/elastic/elasticsearch_enabled_cache.rb'
- 'ee/lib/gitlab/elastic/group_search_results.rb'
- 'ee/lib/gitlab/elastic/helper.rb'
- 'ee/lib/gitlab/elastic/search_results.rb'
- 'ee/lib/gitlab/geo/event_gap_tracking.rb'

View File

@ -10,7 +10,6 @@ Style/RedundantReturn:
- 'ee/app/controllers/groups/analytics/cycle_analytics/stages_controller.rb'
- 'ee/app/controllers/groups/analytics/cycle_analytics/summary_controller.rb'
- 'ee/app/controllers/groups/analytics/tasks_by_type_controller.rb'
- 'ee/app/controllers/groups/epics_controller.rb'
- 'ee/app/controllers/projects/integrations/jira/issues_controller.rb'
- 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb'
- 'ee/app/controllers/projects/on_demand_scans_controller.rb'

View File

@ -1 +1 @@
3d9b3113d69fbe1c51b3a2b8eb95b08601798801
e800fc20f2954c64f42f560e7255db5bf189010b

View File

@ -40,12 +40,12 @@ export default {
<template>
<div class="gl-contents">
<dt class="gl-mb-5 gl-mr-6 gl-max-w-26" data-testid="label-slot">
<dt class="gl-max-w-26" data-testid="label-slot">
<template v-if="label || $scopedSlots.label">
<slot name="label">{{ label }}</slot>
</template>
</dt>
<dd class="gl-mb-5" data-testid="value-slot">
<dd class="md:gl-mb-0" data-testid="value-slot">
<template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot>
</template>

View File

@ -17,6 +17,7 @@ import RunnerGroups from './runner_groups.vue';
import RunnerProjects from './runner_projects.vue';
import RunnerTags from './runner_tags.vue';
import RunnerManagers from './runner_managers.vue';
import RunnerJobs from './runner_jobs.vue';
export default {
components: {
@ -30,14 +31,24 @@ export default {
RunnerProjects,
RunnerTags,
RunnerManagers,
RunnerJobs,
TimeAgo,
},
props: {
runnerId: {
type: String,
required: true,
},
runner: {
type: Object,
required: false,
default: null,
},
showAccessHelp: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
maximumTimeout() {
@ -87,8 +98,10 @@ export default {
<template>
<div v-if="runner">
<div class="gl-pt-4">
<dl class="gl-mb-0 gl-grid gl-grid-cols-[auto_1fr]">
<div class="md:gl-columns-2">
<dl
class="gl-mb-0 gl-flex gl-flex-col gl-gap-x-5 gl-gap-y-1 md:gl-grid md:gl-grid-cols-[auto_1fr] md:gl-gap-y-3"
>
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
<runner-detail
:label="s__('Runners|Last contact')"
@ -143,10 +156,11 @@ export default {
</dl>
</div>
<div class="gl-mt-3 gl-flex gl-flex-col gl-gap-5">
<runner-managers :runner="runner" />
<div class="gl-mt-6 gl-flex gl-flex-col gl-gap-5">
<runner-groups v-if="isGroupRunner" :runner="runner" />
<runner-projects v-if="isProjectRunner" :runner="runner" />
<runner-managers :runner="runner" />
<runner-jobs :runner-id="runnerId" :show-access-help="showAccessHelp" />
</div>
</div>
</template>

View File

@ -1,6 +1,7 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import { s__, sprintf } from '~/locale';
import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
import { formatRunnerName } from '../utils';
import RunnerCreatedAt from './runner_created_at.vue';
@ -28,7 +29,9 @@ export default {
},
computed: {
name() {
return formatRunnerName(this.runner);
return sprintf(s__('Runners|Runner %{name}'), {
name: formatRunnerName(this.runner),
});
},
},
I18N_LOCKED_RUNNER_DESCRIPTION,

View File

@ -87,7 +87,6 @@ export default {
icon="pipeline"
:count="jobs.count"
:is-loading="loading"
class="gl-mt-5"
>
<template v-if="showAccessHelp" #count>
<help-popover>

View File

@ -21,7 +21,7 @@ export default {
<template>
<gl-empty-state
:svg-path="$options.EMPTY_STATE_SVG_URL"
:svg-height="150"
:svg-height="96"
:title="$options.i18n.title"
>
<template #description>

View File

@ -1,9 +1,12 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url';
import { GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui';
import { s__, formatNumber } from '~/locale';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { tableField } from '../utils';
import { RUNNER_MANAGERS_HELP_URL } from '../constants';
import RunnerManagersTable from './runner_managers_table.vue';
export default {
@ -11,6 +14,7 @@ export default {
components: {
GlLink,
GlSprintf,
GlEmptyState,
RunnerManagersTable,
CrudComponent,
HelpPopover,
@ -44,12 +48,13 @@ export default {
thClasses: ['gl-text-right'],
}),
],
RUNNER_MANAGERS_HELP_URL,
EMPTY_STATE_SVG_URL,
};
</script>
<template>
<crud-component
v-if="count > 0"
:title="s__('Runners|Runners')"
icon="container-image"
:count="formattedCount"
@ -78,6 +83,22 @@ export default {
</help-popover>
</template>
<runner-managers-table :items="items" />
<runner-managers-table v-if="count > 0" :items="items" />
<gl-empty-state
v-else
:svg-path="$options.EMPTY_STATE_SVG_URL"
:svg-height="96"
:title="s__('Runners|No runners managers found')"
>
<template #description>
<p>
{{
s__(
'Runners|Runner managers registered under this configuration are listed here. Register and start at least one runner manager.',
)
}}
</p>
</template>
</gl-empty-state>
</crud-component>
</template>

View File

@ -13,7 +13,6 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo
import RunnerHeader from './runner_header.vue';
import RunnerHeaderActions from './runner_header_actions.vue';
import RunnerDetails from './runner_details.vue';
import RunnerJobs from './runner_jobs.vue';
export default {
name: 'RunnerShow',
@ -21,7 +20,6 @@ export default {
RunnerHeader,
RunnerHeaderActions,
RunnerDetails,
RunnerJobs,
},
props: {
runnerId: {
@ -83,7 +81,6 @@ export default {
</template>
</runner-header>
<runner-details :runner="runner" />
<runner-jobs :runner-id="runnerId" :show-access-help="showAccessHelp" />
<runner-details :runner-id="runnerId" :runner="runner" :show-access-help="showAccessHelp" />
</div>
</template>

View File

@ -1,21 +1,13 @@
<script>
import { GlCollapsibleListbox, GlBadge, GlPopover } from '@gitlab/ui';
import { GlCollapsibleListbox } from '@gitlab/ui';
import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import { ACCESS_LEVEL_PLANNER_STRING } from '~/access_level/constants';
export default {
components: { GlCollapsibleListbox, GlBadge, GlPopover },
components: { GlCollapsibleListbox },
inject: {
manageMemberRolesPath: { default: null },
},
i18n: {
plannerRoleDescription: s__(
'MemberRole|The Planner role is a hybrid of the existing Guest and Reporter roles but designed for users who need access to planning workflows.',
),
},
plannerRole: ACCESS_LEVEL_PLANNER_STRING,
badgeId: 'planner-role-badge',
props: {
roles: {
type: Object,
@ -72,14 +64,6 @@ export default {
data-testid="role-data"
>
<span data-testid="role-name">{{ item.text }}</span>
<template v-if="$options.plannerRole === item.value">
<gl-badge :id="$options.badgeId" variant="info" class="gl-float-right gl-ml-2">
{{ __('New') }}
</gl-badge>
<gl-popover :target="$options.badgeId">
{{ $options.i18n.plannerRoleDescription }}
</gl-popover>
</template>
</div>
<div
v-if="item.memberRoleId"

View File

@ -1,7 +1,6 @@
import { groupMemberRequestFormatter } from '~/groups/members/utils';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteGroupsModal from '~/invite_members/init_invite_groups_modal';
import { initPlannerRoleBanner } from '~/planner_role_banner';
import { s__ } from '~/locale';
import { initMembersApp } from '~/members';
import { CONTEXT_TYPE, GROUPS_APP_OPTIONS, MEMBERS_TAB_TYPES } from 'ee_else_ce/members/constants';
@ -60,7 +59,6 @@ const APP_OPTIONS = {
...GROUPS_APP_OPTIONS,
};
initPlannerRoleBanner();
initMembersApp(
document.querySelector('.js-group-members-list-app'),
CONTEXT_TYPE.GROUP,

View File

@ -7,13 +7,11 @@ import initImportProjectMembersTrigger from '~/invite_members/init_import_projec
import initImportProjectMembersModal from '~/invite_members/init_import_project_members_modal';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteGroupsModal from '~/invite_members/init_invite_groups_modal';
import { initPlannerRoleBanner } from '~/planner_role_banner';
import { s__ } from '~/locale';
import { initMembersApp } from '~/members';
import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
initPlannerRoleBanner();
initImportProjectMembersModal();
initInviteGroupsModal();
initInviteGroupTrigger();

View File

@ -1,68 +0,0 @@
<script>
import { GlBanner, GlLink, GlSprintf } from '@gitlab/ui';
import ADD_USER_SVG_URL from '@gitlab/svgs/dist/illustrations/add-user-sm.svg';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
export default {
name: 'PlannerRoleBanner',
i18n: {
title: s__('MemberRole|New Planner role'),
buttonText: s__('MemberRole|Learn more about roles and permissions'), // This is hidden but it is required prop
description:
s__(`MemberRole|The Planner role is a hybrid of the existing Guest and Reporter roles but designed for users who need access to planning workflows. For more information about the new role, see %{blogLinkStart}our blog%{blogLinkEnd} or %{learnMoreStart}learn more about roles and permissions%{learnMoreEnd}.
`),
dismissLabel: s__('MemberRole|Dismiss Planner role promotion'),
},
blogURL:
'https://about.gitlab.com/blog/2024/11/25/introducing-gitlabs-new-planner-role-for-agile-planning-teams/',
docsUrl: helpPagePath('user/permissions'),
ADD_USER_SVG_URL,
buttonAttributes: {
class: 'planner-role-banner-button',
},
components: {
GlBanner,
UserCalloutDismisser,
GlLink,
GlSprintf,
},
};
</script>
<template>
<user-callout-dismisser feature-name="planner_role_callout">
<template #default="{ dismiss, shouldShowCallout }">
<div v-if="shouldShowCallout" class="gl-pt-5">
<gl-banner
:title="$options.i18n.title"
:button-text="$options.i18n.buttonText"
:button-link="$options.docsUrl"
:svg-path="$options.ADD_USER_SVG_URL"
:button-attributes="$options.buttonAttributes"
:dismiss-label="$options.i18n.dismissLabel"
data-testid="planner-role-banner"
variant="promotion"
@close="dismiss"
>
<span>
<gl-sprintf :message="$options.i18n.description">
<template #blogLink="{ content }">
<gl-link :href="$options.blogURL" target="_blank">{{ content }}</gl-link>
</template>
<template #learnMore="{ content }">
<gl-link :href="$options.docsUrl">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</gl-banner>
</div>
</template>
</user-callout-dismisser>
</template>
<!-- As per the requirement, the link button is not required and banner is not support hiding the link button -->
<style>
.planner-role-banner-button {
display: none;
}
</style>

View File

@ -1,34 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import PlannerRoleBanner from './components/planner_role_banner.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const EXPIRY_DATE = '2025-06-14';
export const initPlannerRoleBanner = () => {
const expiryDate = new Date(EXPIRY_DATE);
if (Date.now() > expiryDate) {
return null;
}
const el = document.getElementById('js-planner-role-banner');
if (!el) {
return null;
}
return new Vue({
el,
name: 'PlannerRoleBannerRoot',
apolloProvider,
render(h) {
return h(PlannerRoleBanner);
},
});
};

View File

@ -11,10 +11,26 @@ module Mutations
description 'Allows updating several properties for a set of work items. '
argument :assignees_widget,
::Types::WorkItems::Widgets::AssigneesInputType,
required: false,
description: 'Input for assignees widget.',
experiment: { milestone: '18.2' }
argument :confidential,
GraphQL::Types::Boolean,
required: false,
description: 'Sets the work item confidentiality.',
experiment: { milestone: '18.2' }
argument :ids, [::Types::GlobalIDType[::WorkItem]],
required: true,
description: 'Global ID array of the issues that will be updated. ' \
"IDs that the user can\'t update will be ignored. A max of #{MAX_WORK_ITEMS} can be provided."
argument :milestone_widget,
::Types::WorkItems::Widgets::MilestoneInputType,
required: false,
description: 'Input for milestone widget.',
experiment: { milestone: '18.2' }
argument :parent_id, ::Types::GlobalIDType[::WorkItems::Parent],
required: true,
description: 'Global ID of the parent to which the bulk update will be scoped. ' \
@ -24,8 +40,7 @@ module Mutations
argument :labels_widget,
::Types::WorkItems::Widgets::LabelsUpdateInputType,
required: false,
description: 'Input for labels widget.',
prepare: ->(input, _) { input.to_h }
description: 'Input for labels widget.'
field :updated_work_item_count, GraphQL::Types::Int,
null: true,
@ -50,7 +65,7 @@ module Mutations
parent: parent,
current_user: current_user,
work_item_ids: ids.map(&:model_id),
widget_params: attributes
attributes: attributes
).execute
if result.success?

View File

@ -97,7 +97,7 @@ module Users
openssl_callout: 94,
# 95 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170868
new_mr_dashboard_banner: 96,
planner_role_callout: 97,
# 97 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196130
# EE-only
pipl_compliance_alert: 98,
new_merge_request_dashboard_welcome: 99,

View File

@ -2,11 +2,11 @@
module WorkItems
class BulkUpdateService
def initialize(parent:, current_user:, work_item_ids:, widget_params: {})
def initialize(parent:, current_user:, work_item_ids:, attributes: {})
@parent = parent
@work_item_ids = work_item_ids
@current_user = current_user
@widget_params = widget_params.dup
@attributes = attributes
end
def execute
@ -14,19 +14,30 @@ module WorkItems
return ServiceResponse.error(message: "User can't read parent", reason: :authorization)
end
updated_work_items = scoped_work_items.find_each(batch_size: 100) # rubocop:disable CodeReuse/ActiveRecord -- Implementation would be identical in model
.filter_map do |work_item|
next unless @current_user.can?(:update_work_item, work_item)
non_widget_attributes = @attributes.except(*all_widget_keys)
update_result = WorkItems::UpdateService.new(
container: work_item.resource_parent,
widget_params: @widget_params,
current_user: @current_user
).execute(work_item)
updated_work_items = scoped_work_items
.find_each(batch_size: 100) # rubocop:disable CodeReuse/ActiveRecord -- Implementation would be identical in model
.filter_map do |work_item|
next unless @current_user.can?(:update_work_item, work_item)
work_item if update_result[:status] == :success
end
widget_params = extract_supported_widget_params(
work_item.work_item_type,
@attributes,
work_item.resource_parent
)
# Skip if no applicable widgets for this work item type
next if widget_params.blank? && non_widget_attributes.blank?
update_result = WorkItems::UpdateService.new(
container: work_item.resource_parent,
widget_params: widget_params,
params: non_widget_attributes,
current_user: @current_user
).execute(work_item)
work_item if update_result[:status] == :success
end
ServiceResponse.success(payload: { updated_work_item_count: updated_work_items.count })
end
@ -36,7 +47,10 @@ module WorkItems
ids = WorkItem.id_in(@work_item_ids)
cte = Gitlab::SQL::CTE.new(:work_item_ids_cte, ids)
work_item_scope = WorkItem.all
cte.apply_to(work_item_scope).in_namespaces_with_cte(namespaces)
cte
.apply_to(work_item_scope)
.in_namespaces_with_cte(namespaces)
.includes(:work_item_type) # rubocop:disable CodeReuse/ActiveRecord -- Implementation would be identical in model
end
def namespaces
@ -58,5 +72,22 @@ module WorkItems
Project.in_namespace(@parent.self_and_descendant_ids)
end.select('projects.project_namespace_id as id')
end
def all_widget_keys
@all_widget_keys ||= ::WorkItems::WidgetDefinition.available_widgets.map(&:api_symbol)
end
def extract_supported_widget_params(work_item_type, attributes, resource_parent)
supported_widget_keys = work_item_type.widget_classes(resource_parent).map(&:api_symbol)
keys_to_extract = all_widget_keys & attributes.keys & supported_widget_keys
return {} if keys_to_extract.empty?
widget_params = attributes.slice(*keys_to_extract)
widget_params.transform_values do |input|
input.is_a?(Array) ? input.map(&:to_h) : input.to_h
end
end
end
end

View File

@ -4,7 +4,6 @@
- content_for :hide_invite_members_button, true
- if can_admin_group_member?(@group)
= render_if_exists 'shared/promotions/promote_planner_role'
= render_if_exists 'groups/group_members/link_to_pending_members'
= render ::Layouts::PageHeadingComponent.new(_('Group members')) do |c|
- c.with_description do

View File

@ -72,7 +72,7 @@
- c.with_description do
= s_('Preferences|Customize the behavior of the system layout and default views.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'behavior'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'behavior'), target: '_blank', rel: 'noopener noreferrer', class: 'gl-link-inline'
- c.with_body do
.form-group
= f.label :keyboard_shortcuts_enabled, class: 'label-bold' do
@ -147,11 +147,11 @@
- c.with_description do
= _('Customize language and region related settings.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'localization'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'localization'), target: '_blank', rel: 'noopener noreferrer', class: 'gl-link-inline'
- c.with_body do
.js-listbox-input{ data: { label: _('Language'), description: s_('Preferences|This feature is experimental and translations are not yet complete.'), name: 'user[preferred_language]', items: language_choices.to_json, value: current_user.preferred_language, block: true.to_s, toggle_class: 'gl-form-input-xl' } }
%p.-gl-mt-5
= link_to help_page_url('development/i18n/translation.md'), class: 'text-nowrap', target: '_blank', rel: 'noopener noreferrer' do
= link_to help_page_url('development/i18n/translation.md'), class: 'text-nowrap gl-link-inline', target: '_blank', rel: 'noopener noreferrer' do
= _("Help translate GitLab into your language")
%span{ aria: { label: _('Open new window') } }
= sprite_icon('external-link')
@ -166,7 +166,7 @@
- c.with_description do
= s_('Preferences|Configure how dates and times display for you.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'show-exact-times-instead-of-relative-times'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'show-exact-times-instead-of-relative-times'), target: '_blank', rel: 'noopener noreferrer', class: 'gl-link-inline'
- c.with_body do
.form-group
= f.gitlab_ui_checkbox_component :time_display_relative,
@ -185,7 +185,7 @@
- c.with_description do
= s_('Preferences|Turns on or off the ability to follow or be followed by other users.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/profile/_index.md', anchor: 'follow-users'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('Learn more'), help_page_path('user/profile/_index.md', anchor: 'follow-users'), target: '_blank', rel: 'noopener noreferrer', class: 'gl-link-inline'
- c.with_body do
.form-group
= f.gitlab_ui_checkbox_component :enabled_following,

View File

@ -4,7 +4,6 @@
- content_for :reload_on_member_invite_success, true
- content_for :hide_invite_members_button, true
= render_if_exists 'shared/promotions/promote_planner_role'
- if show_the_header
= render ::Layouts::PageHeadingComponent.new(_("Project members")) do |c|
- c.with_description do

View File

@ -1 +0,0 @@
#js-planner-role-banner

View File

@ -12,8 +12,10 @@ class MergeRequests::DeleteSourceBranchWorker
idempotent!
def perform(merge_request_id, source_branch_sha, user_id)
merge_request = MergeRequest.find(merge_request_id)
user = User.find(user_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
user = User.find_by_id(user_id)
return unless merge_request && user
# Source branch changed while it's being removed
return if merge_request.source_branch_sha != source_branch_sha
@ -22,6 +24,5 @@ class MergeRequests::DeleteSourceBranchWorker
.execute(merge_request)
::Projects::DeleteBranchWorker.new.perform(merge_request.source_project.id, user_id, merge_request.source_branch)
rescue ActiveRecord::RecordNotFound
end
end

View File

@ -13,10 +13,11 @@ class MergeRequests::ResolveTodosWorker
idempotent!
def perform(merge_request_id, user_id)
merge_request = MergeRequest.find(merge_request_id)
user = User.find(user_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
user = User.find_by_id(user_id)
return unless merge_request && user
MergeRequests::ResolveTodosService.new(merge_request, user).execute
rescue ActiveRecord::RecordNotFound
end
end

View File

@ -5,7 +5,7 @@ description: >
This is the second attempt to initialize the values that were missed in a time window just after
BackfillOrganizationIdOnCiRunnerMachines was started.
feature_category: fleet_visibility
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195033
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195123
milestone: '18.2'
queued_migration_version: 20250620094653
finalized_by: # version of the migration that finalized this BBM

View File

@ -8,14 +8,6 @@ description: Helm package file metadata
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57017
milestone: '13.12'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: package_file_id
table: packages_package_files
sharding_key: project_id
belongs_to: package_file
sharding_key:
project_id: projects
table_size: small
desired_sharding_key_migration_job_name: BackfillPackagesHelmFileMetadataProjectId

View File

@ -8,14 +8,6 @@ description: Join table between nuget target frameworks and packages_dependency_
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30618
milestone: '13.0'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dependency_link_id
table: packages_dependency_links
sharding_key: project_id
belongs_to: dependency_link
sharding_key:
project_id: projects
table_size: small
desired_sharding_key_migration_job_name: BackfillPackagesNugetDependencyLinkMetadataProjectId

View File

@ -4,7 +4,7 @@ class CreateAiCatalogItems < Gitlab::Database::Migration[2.3]
milestone '18.2'
def change
create_table :ai_catalog_items do |t| # rubocop:disable Migration/EnsureFactoryForTable -- False Positive, does exist in ee/spec/factories/ai/catalog/item.rb
create_table :ai_catalog_items do |t|
t.bigint :organization_id, index: true, null: false
t.bigint :project_id, index: true
t.timestamps_with_timezone null: false

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddPackagesHelmFileMetadataProjectIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
def up
add_not_null_constraint :packages_helm_file_metadata, :project_id
end
def down
remove_not_null_constraint :packages_helm_file_metadata, :project_id
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddPackagesNugetDependencyLinkMetadataProjectIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.2'
disable_ddl_transaction!
def up
add_not_null_constraint :packages_nuget_dependency_link_metadata, :project_id
end
def down
remove_not_null_constraint :packages_nuget_dependency_link_metadata, :project_id
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddTextLimitToOauthApplicationsScopes < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '18.2'
def up
# Add constraint without validation to ensure consistency while maintaining compatibility
# Validation will be enabled in a follow-up migration in 18.3
add_text_limit :oauth_applications, :scopes, 2048, validate: false
end
def down
remove_text_limit :oauth_applications, :scopes
end
end

View File

@ -0,0 +1 @@
01a17dc8570de3cda39ec70f85d8631d78d4532244b9f39600896c24ac6cb912

View File

@ -0,0 +1 @@
31a2ae1ff707859c660ce4e011ea40b06a2d64c922343e388f904b9c5d6f01f0

View File

@ -0,0 +1 @@
dc0151b8d870ed1be417504681f73f1ec6234c94890ca3e0fe584de175f01d7b

View File

@ -19795,7 +19795,8 @@ CREATE TABLE packages_helm_file_metadata (
channel text NOT NULL,
metadata jsonb,
project_id bigint,
CONSTRAINT check_06e8d100af CHECK ((char_length(channel) <= 255))
CONSTRAINT check_06e8d100af CHECK ((char_length(channel) <= 255)),
CONSTRAINT check_109d878e47 CHECK ((project_id IS NOT NULL))
);
CREATE TABLE packages_maven_metadata (
@ -19858,6 +19859,7 @@ CREATE TABLE packages_nuget_dependency_link_metadata (
dependency_link_id bigint NOT NULL,
target_framework text NOT NULL,
project_id bigint,
CONSTRAINT check_1c3e07cfff CHECK ((project_id IS NOT NULL)),
CONSTRAINT packages_nuget_dependency_link_metadata_target_framework_constr CHECK ((char_length(target_framework) <= 255))
);
@ -29751,6 +29753,9 @@ ALTER TABLE ONLY instance_type_ci_runners
ALTER TABLE ONLY project_type_ci_runners
ADD CONSTRAINT check_619c71f3a2 UNIQUE (id);
ALTER TABLE oauth_applications
ADD CONSTRAINT check_75750847b8 CHECK ((char_length(scopes) <= 2048)) NOT VALID;
ALTER TABLE ONLY group_type_ci_runners
ADD CONSTRAINT check_81b90172a6 UNIQUE (id);

View File

@ -12953,9 +12953,12 @@ Input type: `WorkItemBulkUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitembulkupdateassigneeswidget"></a>`assigneesWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetAssigneesInput`](#workitemwidgetassigneesinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitembulkupdateconfidential"></a>`confidential` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdateids"></a>`ids` | [`[WorkItemID!]!`](#workitemid) | Global ID array of the issues that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
| <a id="mutationworkitembulkupdatelabelswidget"></a>`labelsWidget` | [`WorkItemWidgetLabelsUpdateInput`](#workitemwidgetlabelsupdateinput) | Input for labels widget. |
| <a id="mutationworkitembulkupdatemilestonewidget"></a>`milestoneWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
| <a id="mutationworkitembulkupdateparentid"></a>`parentId` | [`WorkItemsParentID!`](#workitemsparentid) | Global ID of the parent to which the bulk update will be scoped. The parent can be a project. The parent can also be a group (Premium and Ultimate only). Example `WorkItemsParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`. |
#### Fields
@ -47294,7 +47297,6 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumpipeline_new_inputs_adoption_banner"></a>`PIPELINE_NEW_INPUTS_ADOPTION_BANNER` | Callout feature name for pipeline_new_inputs_adoption_banner. |
| <a id="usercalloutfeaturenameenumpipeline_schedules_inputs_adoption_banner"></a>`PIPELINE_SCHEDULES_INPUTS_ADOPTION_BANNER` | Callout feature name for pipeline_schedules_inputs_adoption_banner. |
| <a id="usercalloutfeaturenameenumpipl_compliance_alert"></a>`PIPL_COMPLIANCE_ALERT` | Callout feature name for pipl_compliance_alert. |
| <a id="usercalloutfeaturenameenumplanner_role_callout"></a>`PLANNER_ROLE_CALLOUT` | Callout feature name for planner_role_callout. |
| <a id="usercalloutfeaturenameenumpreview_user_over_limit_free_plan_alert"></a>`PREVIEW_USER_OVER_LIMIT_FREE_PLAN_ALERT` | Callout feature name for preview_user_over_limit_free_plan_alert. |
| <a id="usercalloutfeaturenameenumproduct_analytics_dashboard_feedback"></a>`PRODUCT_ANALYTICS_DASHBOARD_FEEDBACK` | Callout feature name for product_analytics_dashboard_feedback. |
| <a id="usercalloutfeaturenameenumproduct_usage_data_collection_changes"></a>`PRODUCT_USAGE_DATA_COLLECTION_CHANGES` | Callout feature name for product_usage_data_collection_changes. |

View File

@ -12,12 +12,14 @@ title: Repository submodules API
{{< /details >}}
Use this API to update a
[Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) reference in a specific
branch of your Git repository.
## Update existing submodule reference in repository
In some workflows, especially automated ones, you can update a
submodule's reference to keep up to date other projects that use it.
This endpoint allows you to update a [Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) reference in a
specific branch.
```plaintext
PUT /projects/:id/repository/submodules/:submodule

View File

@ -6,6 +6,9 @@ description: Settings and commands in the GitLab Workflow extension for VS Code.
title: GitLab Workflow extension settings and commands
---
The GitLab Workflow extension for VS Code integrates with the VS Code Command Palette, extends existing
VS Code integrations with Git, and provides configuration options.
## Command Palette commands
This extension provides several sets of commands that you can trigger in the
@ -88,15 +91,15 @@ If you use self-signed certificates to connect to your GitLab instance, read the
| Setting | Default | Information |
| ------- | ------- | ----------- |
| `gitlab.customQueries` | Not applicable | Defines the search queries that retrieves the items shown on the GitLab Panel. For more information, see [Custom Queries documentation](custom_queries.md). |
| `gitlab.debug` | false | Set to `true` to enable debug mode. Debug mode improves error stack traces because the extension uses source maps to understand minified code. Debug mode also shows debug log messages in the [extension logs](troubleshooting.md#view-log-files). |
| `gitlab.duo.enabledWithoutGitlabProject` | true | Set to `true` to keep GitLab Duo features enabled if the extension can't retrieve the project's `duoFeaturesEnabledForProject` setting. When `false`, all GitLab Duo features are disabled if the extension can't retrieve the project's `duoFeaturesEnabledForProject` setting. See [`duoFeaturesEnabledForProject` setting](#duofeaturesenabledforproject). |
| `gitlab.debug` | false | When `true`, enables debug mode. Debug mode improves error stack traces because the extension uses source maps to understand minified code. Debug mode also shows debug log messages in the [extension logs](troubleshooting.md#view-log-files). |
| `gitlab.duo.enabledWithoutGitlabProject` | true | When `true`, keeps GitLab Duo features enabled if the extension can't retrieve the project's `duoFeaturesEnabledForProject` setting. When `false`, disables all GitLab Duo features if the extension can't retrieve the project's `duoFeaturesEnabledForProject` setting. See [`duoFeaturesEnabledForProject` setting](#duofeaturesenabledforproject). |
| `gitlab.duoCodeSuggestions.additionalLanguages` | Not applicable | (Experimental.) To expand the list of [officially supported languages](../../user/project/repository/code_suggestions/supported_extensions.md#supported-languages-by-ide) for Code Suggestions, provide an array of the [language identifiers](https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers). Code suggestions quality for the added languages might not be optimal. |
| `gitlab.duoCodeSuggestions.enabled` | true | Toggle to enable or disable AI-assisted code suggestions. |
| `gitlab.duoCodeSuggestions.enabled` | true | When `true`, enables AI-assisted code suggestions. |
| `gitlab.duoCodeSuggestions.enabledSupportedLanguages` | Not applicable | The [supported languages](../../user/project/repository/code_suggestions/supported_extensions.md#supported-languages-by-ide) for which to enable Code Suggestions. By default, all supported languages are enabled. |
| `gitlab.duoCodeSuggestions.openTabsContext` | true | Toggle to enable or disable sending of context across open tabs to improve Code Suggestions. |
| `gitlab.keybindingHints.enabled"` | true | Enable keybinding hints for GitLab Duo. |
| `gitlab.pipelineGitRemoteName` | null | The name of the Git remote name corresponding to the GitLab repository with your pipelines. If set to `null` or missing, then the extension uses the same remote as for the non-pipeline features. |
| `gitlab.showPipelineUpdateNotifications` | false | Set to `true` to show an alert when a pipeline completes. |
| `gitlab.duoCodeSuggestions.openTabsContext` | true | When `true`, enables sending of context across open tabs to improve Code Suggestions. |
| `gitlab.keybindingHints.enabled` | true | Enables keybinding hints for GitLab Duo. |
| `gitlab.pipelineGitRemoteName` | null | The name of the Git remote name corresponding to the GitLab repository with your pipelines. When `null` or empty, then the extension uses the same remote as for the non-pipeline features. |
| `gitlab.showPipelineUpdateNotifications` | false | When `true`, shows an alert when a pipeline completes. |
### `duoFeaturesEnabledForProject`

View File

@ -92,7 +92,7 @@ These variables tell the scanner how to authenticate with your application.
| `DAST_AUTH_PASSWORD` | String | `P@55w0rd!` | The password to authenticate to in the website. |
| `DAST_AUTH_PASSWORD_FIELD` | [selector](authentication.md#finding-an-elements-selector) | `name:password` | A selector describing the element used to enter the password on the login form. |
| `DAST_AUTH_SUBMIT_FIELD` | [selector](authentication.md#finding-an-elements-selector) | `css:input[type=submit]` | A selector describing the element clicked on to submit the login form for a single-page login form, or the password form for a multi-page login form. |
| `DAST_AUTH_SUCCESS_IF_AT_URL` | URL | `https://www.site.com/welcome` | A URL that is compared to the URL in the browser to determine if authentication has succeeded after the login form is submitted. |
| `DAST_AUTH_SUCCESS_IF_AT_URL` | URL | `https://www.site.com/welcome*` | A URL that is compared to the URL in the browser to determine if authentication has succeeded after the login form is submitted. Wildcard `*` can be used to match a dynamic URL. |
| `DAST_AUTH_SUCCESS_IF_ELEMENT_FOUND` | [selector](authentication.md#finding-an-elements-selector) | `css:.user-avatar` | A selector describing an element whose presence is used to determine if authentication has succeeded after the login form is submitted. |
| `DAST_AUTH_SUCCESS_IF_NO_LOGIN_FORM` | boolean | `true` | Verifies successful authentication by checking for the absence of a login form after the login form has been submitted. This success check is enabled by default. |
| `DAST_AUTH_TYPE` | string | `basic-digest` | The authentication type to use. |

View File

@ -860,6 +860,35 @@ policy::container-security:
- echo "CS_IMAGE:$CS_IMAGE"
```
### Customize enforced jobs using `.gitlab-ci.yml` and artifacts
Because policy pipelines run in isolation, pipeline execution policies cannot read variables from `.gitlab-ci.yml` directly.
If you want to use the variables in `.gitlab-ci.yml` instead of defining them in the project's CI/CD configuration,
you can use artifacts to pass variables from the `.gitlab-ci.yml` configuration to the pipeline execution policy's pipeline.
```yaml
# .gitlab-ci.yml
build-job:
stage: build
script:
- echo "BUILD_VARIABLE=value_from_build_job" >> build.env
artifacts:
reports:
dotenv: build.env
```
```yaml
stages:
- build
- test
test-job:
stage: test
script:
- echo "$BUILD_VARIABLE" # Prints "value_from_build_job"
```
### Customize security scanner's behavior with `before_script` in project configurations
To customize the behavior of a security job enforced by a policy in the project's `.gitlab-ci.yml`, you can override `before_script`.
@ -902,6 +931,55 @@ secret_detection:
By using `override_project_ci` and including the project's configuration, it allows for YAML configurations to be merged.
### Configure resource-specific variable control
You can allow teams to set global variables that can override pipeline execution policy variables, while still permitting job-specific overrides. This allows teams to set appropriate defaults for security scans, but use appropriate resources for other jobs.
Include in your `resource-optimized-scans.yml`:
```yaml
variables:
# Default resource settings for all jobs
KUBERNETES_MEMORY_REQUEST: 4Gi
KUBERNETES_MEMORY_LIMIT: 4Gi
# Default values that teams can override via project variables
SAST_KUBERNETES_MEMORY_REQUEST: 4Gi
sast:
variables:
SAST_EXCLUDED_ANALYZERS: 'spotbugs'
KUBERNETES_MEMORY_REQUEST: $SAST_KUBERNETES_MEMORY_REQUEST
KUBERNETES_MEMORY_LIMIT: $SAST_KUBERNETES_MEMORY_REQUEST
```
Include in your `policy.yml`:
```yaml
pipeline_execution_policy:
- name: Resource-Optimized Security Policy
description: Enforces security scans with efficient resource management
enabled: true
pipeline_config_strategy: inject_ci
content:
include:
- project: security/policy-templates
file: resource-optimized-scans.yml
ref: main
variables_override:
allowed: false
exceptions:
# Allow scan-specific resource overrides
- SAST_KUBERNETES_MEMORY_REQUEST
- SECRET_DETECTION_KUBERNETES_MEMORY_REQUEST
- CS_KUBERNETES_MEMORY_REQUEST
# Allow necessary scan customization
- CS_IMAGE
- SAST_EXCLUDED_PATHS
```
This approach allows teams to set scan-specific resource variables (like `SAST_KUBERNETES_MEMORY_REQUEST`) using variable overrides without affecting all jobs in their pipeline, which provides better resource management for large projects. This example also shows the use of other common scan customization options that you can extend to developers. Make sure you document the available variables so your development teams can leverage them.
### Use group or project variables in a pipeline execution policy
You can use group or project variables in a pipeline execution policy.
@ -993,3 +1071,24 @@ include:
- 'Dockerfile'
project: '$CI_PROJECT_PATH'
```
To use this approach, the group or project must use the `override_project_ci` strategy.
### Enforce a container scanning `component` using a pipeline execution policy
You can use security scan components to improve the handling and enforcement of versioning.
```yaml
include:
- component: gitlab.com/components/container-scanning/container-scanning@main
inputs:
cs_image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
container_scanning: # override component with additional configuration
variables:
CS_REGISTRY_USER: $CI_REGISTRY_USER
CS_REGISTRY_PASSWORD: $CI_REGISTRY_PASSWORD
SECURE_LOG_LEVEL: debug # add for verbose debugging of the container scanner
before_script:
- echo $CS_IMAGE # optionally add a before_script for additional debugging
```

View File

@ -236,17 +236,10 @@ To show the sidebar again:
{{< history >}}
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4231) in GitLab 17.4 [with a flag](../../../administration/feature_flags/_index.md) named `work_items_beta`. Disabled by default. This feature is in [beta](../../../policy/development_stages_support.md#beta).
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/551805) in GitLab 18.2.
{{< /history >}}
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}
An epic can be assigned to one or more users.
The assignees can be changed as often as needed.
@ -255,10 +248,6 @@ The idea is that the assignees are people responsible for the epic.
If a user is not a member of a group, an epic can only be assigned to them if another group member
assigns them.
This feature is in [beta](../../../policy/development_stages_support.md).
If you find a bug, use the
[feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/463598) to provide more details.
### Change assignee on an epic
{{< history >}}

View File

@ -68,9 +68,9 @@ Your `.gitlab-ci.yml` should look similar to this:
```yaml
build:
image: $CI_REGISTRY/group/project/docker:20.10.16
image: $CI_REGISTRY/group/project/docker:24.0.5
services:
- name: $CI_REGISTRY/group/project/docker:20.10.16-dind
- name: $CI_REGISTRY/group/project/docker:24.0.5-dind
alias: docker
stage: build
script:
@ -97,9 +97,9 @@ Your `.gitlab-ci.yml` should look similar to this:
```yaml
build:
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:20.10.16
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:24.0.5
services:
- name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:18.09.7-dind
- name: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/docker:24.0.5-dind
alias: docker
stage: build
script:
@ -120,10 +120,10 @@ If you're using Docker-in-Docker on your runners, your `.gitlab-ci.yml` file sho
```yaml
build:
image: docker:20.10.16
image: docker:24.0.5
stage: build
services:
- docker:20.10.16-dind
- docker:24.0.5-dind
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
- docker build -t $CI_REGISTRY/group/project/image:latest .
@ -134,10 +134,10 @@ You can use [CI/CD variables](../../../ci/variables/_index.md) in your `.gitlab-
```yaml
build:
image: docker:20.10.16
image: docker:24.0.5
stage: build
services:
- docker:20.10.16-dind
- docker:24.0.5-dind
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
script:
@ -159,9 +159,9 @@ registry and used by subsequent stages, downloading the container image when nee
```yaml
default:
image: docker:20.10.16
image: docker:24.0.5
services:
- docker:20.10.16-dind
- docker:24.0.5-dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin

View File

@ -111,7 +111,7 @@ when you view that file in your project's Git repository:
When you cherry-pick a merge commit in the GitLab UI or API, GitLab adds a [system note](../system_notes.md)
to the related merge request thread. The format is {{< icon name="cherry-pick-commit" >}}
`[USER]` **picked the changes into the branch** `[BRANCHNAME]` with commit** `[SHA]` `[DATE]`:
`[USER]` **picked the changes into the branch** `[BRANCHNAME]` with commit `[SHA]` `[DATE]`:
![Cherry-pick tracking in merge request timeline](img/cherry_pick_mr_timeline_v15_4.png)

View File

@ -0,0 +1,206 @@
# frozen_string_literal: true
module Gitlab
module Database
class CollationChecker
COLLATION_VERSION_MISMATCH_QUERY = <<~SQL
SELECT
collname AS collation_name,
collprovider AS provider,
collversion AS stored_version,
pg_collation_actual_version(oid) AS actual_version,
collversion <> pg_collation_actual_version(oid) AS has_mismatch
FROM
pg_collation
WHERE
collprovider = 'c'
AND collversion IS NOT NULL
AND pg_collation_actual_version(oid) IS NOT NULL
AND collversion <> pg_collation_actual_version(oid);
SQL
def self.run(database_name: nil, logger: Gitlab::AppLogger)
Gitlab::Database::EachDatabase.each_connection(only: database_name) do |connection, database|
new(connection, database, logger).run
end
end
attr_reader :connection, :database_name, :logger
def initialize(connection, database_name, logger)
@connection = connection
@database_name = database_name
@logger = logger
end
def run
result = { mismatches_found: false, affected_indexes: [] }
logger.info("Checking for PostgreSQL collation mismatches on #{database_name} database...")
mismatched = mismatched_collations
if mismatched.empty?
logger.info("No collation mismatches detected on #{database_name}.")
return result
end
result[:mismatches_found] = true
logger.warn("⚠️ COLLATION MISMATCHES DETECTED on #{database_name} database!")
logger.warn("#{mismatched.count} collation(s) have version mismatches:")
mismatched.each do |row|
logger.warn(
" - #{row['collation_name']}: stored=#{row['stored_version']}, actual=#{row['actual_version']}"
)
end
affected_indexes = find_affected_indexes(mismatched)
if affected_indexes.empty?
logger.info("No indexes appear to be affected by the collation mismatches.")
return result
end
result[:affected_indexes] = affected_indexes
logger.warn("Affected indexes that need to be rebuilt:")
affected_indexes.each do |row|
logger.warn(" - #{row['index_name']} (#{row['index_type']}) on table #{row['table_name']}")
logger.warn(" • Affected columns: #{row['affected_columns']}")
logger.warn(" • Type: #{unique?(row) ? 'UNIQUE' : 'NON-UNIQUE'}")
end
# Provide remediation guidance
provide_remediation_guidance(affected_indexes)
result
end
private
# Helper method to check if an index is unique, handling both string and boolean values
def unique?(index)
unique = index['is_unique']
unique == 't' || unique == true || unique == 'true'
end
def mismatched_collations
connection.select_all(COLLATION_VERSION_MISMATCH_QUERY).to_a
rescue ActiveRecord::StatementInvalid => e
logger.error("Error checking collation mismatches: #{e.message}")
[]
end
def find_affected_indexes(mismatched_collations)
return [] if mismatched_collations.empty?
collation_names = mismatched_collations.map { |row| connection.quote(row['collation_name']) }.join(',')
# Using a more comprehensive query based on PostgreSQL wiki
# Link: https://wiki.postgresql.org/wiki/Locale_data_changes#What_indexes_are_affected
query = <<~SQL
SELECT DISTINCT
indrelid::regclass::text AS table_name,
indexrelid::regclass::text AS index_name,
string_agg(a.attname, ', ' ORDER BY s.attnum) AS affected_columns,
am.amname AS index_type,
s.indisunique AS is_unique
FROM
(SELECT
indexrelid,
indrelid,
indcollation[j] coll,
indkey[j] attnum,
indisunique
FROM
pg_index i,
generate_subscripts(indcollation, 1) g(j)
) s
JOIN
pg_collation c ON coll=c.oid
JOIN
pg_class idx ON idx.oid = s.indexrelid
JOIN
pg_am am ON idx.relam = am.oid
JOIN
pg_attribute a ON a.attrelid = s.indrelid AND a.attnum = s.attnum
WHERE
c.collname IN (#{collation_names})
GROUP BY
s.indexrelid, s.indrelid, s.indisunique, index_name, table_name, am.amname
ORDER BY
table_name,
index_name;
SQL
connection.select_all(query).to_a
rescue ActiveRecord::StatementInvalid => e
logger.error("Error finding affected indexes: #{e.message}")
[]
end
def provide_remediation_guidance(affected_indexes)
log_remediation_header
log_duplicate_entry_checks(affected_indexes)
log_index_rebuild_commands(affected_indexes)
log_collation_refresh_commands
log_conclusion
end
def log_remediation_header
logger.warn("\nREMEDIATION STEPS:")
logger.warn("1. Put GitLab into maintenance mode")
logger.warn("2. Run the following SQL commands:")
end
def log_duplicate_entry_checks(affected_indexes)
# Use the unique? helper method for consistency
unique_indexes = affected_indexes.select { |idx| unique?(idx) }
return unless unique_indexes.any?
logger.warn("\n# Step 1: Check for duplicate entries in unique indexes")
unique_indexes.each do |idx|
logger.warn("-- Check for duplicates in #{idx['table_name']} (unique index: #{idx['index_name']})")
columns = idx['affected_columns'].split(', ')
cols_str = columns.join(', ')
logger.warn(
"SELECT #{cols_str}, COUNT(*), ARRAY_AGG(id) " \
"FROM #{idx['table_name']} " \
"GROUP BY #{cols_str} HAVING COUNT(*) > 1 LIMIT 1;"
)
end
logger.warn("\n# If duplicates exist, you may need to use gitlab:db:deduplicate_tags or similar tasks")
logger.warn("# to fix duplicate entries before rebuilding unique indexes.")
end
def log_index_rebuild_commands(affected_indexes)
return unless affected_indexes.any?
logger.warn("\n# Step 2: Rebuild affected indexes")
logger.warn("# Option A: Rebuild individual indexes with minimal downtime:")
affected_indexes.each do |row|
logger.warn("REINDEX INDEX #{row['index_name']} CONCURRENTLY;")
end
logger.warn("\n# Option B: Alternatively, rebuild all indexes at once (requires downtime):")
logger.warn("REINDEX DATABASE #{database_name};")
end
def log_collation_refresh_commands
# Customer reported this command as working: https://gitlab.com/groups/gitlab-org/-/epics/8573#note_2513370623
logger.warn("\n# Step 3: Refresh collation versions")
logger.warn("ALTER DATABASE #{database_name} REFRESH COLLATION VERSION;")
logger.warn("-- This updates all collation versions in the database to match the current OS")
end
def log_conclusion
logger.warn("\n3. Take GitLab out of maintenance mode")
logger.warn("\nFor more information, see: https://docs.gitlab.com/administration/postgresql/upgrading_os/")
end
end
end
end

View File

@ -150,11 +150,11 @@ module Gitlab
additional: true,
auth: true,
type: "URL",
example: "https://www.site.com/welcome",
example: "https://www.site.com/welcome*",
name: s_("DastProfiles|Success URL"),
description: s_(
"DastProfiles|A URL that is compared to the URL in the browser to determine if authentication " \
"has succeeded after the login form is submitted."
"has succeeded after the login form is submitted. Wildcard `*` can be used to match a dynamic URL."
)
},
DAST_AUTH_SUCCESS_IF_ELEMENT_FOUND: {

View File

@ -598,6 +598,20 @@ namespace :gitlab do
end
end
desc 'GitLab | DB | Check for PostgreSQL collation mismatches and list affected indexes'
task collation_checker: :environment do
Gitlab::Database::CollationChecker.run(logger: Logger.new($stdout))
end
namespace :collation_checker do
each_database(databases) do |database_name|
desc "GitLab | DB | Check for PostgreSQL collation mismatches on the #{database_name} database"
task database_name => :environment do
Gitlab::Database::CollationChecker.run(database_name: database_name, logger: Logger.new($stdout))
end
end
end
namespace :dictionary do
desc 'Generate database docs yaml'
task generate: :environment do

View File

@ -20082,7 +20082,7 @@ msgstr ""
msgid "DastProfiles|/graphql"
msgstr ""
msgid "DastProfiles|A URL that is compared to the URL in the browser to determine if authentication has succeeded after the login form is submitted."
msgid "DastProfiles|A URL that is compared to the URL in the browser to determine if authentication has succeeded after the login form is submitted. Wildcard `*` can be used to match a dynamic URL."
msgstr ""
msgid "DastProfiles|A comma-separated list of actions to take after login but before login verification. Supports `click` and `select` actions. See [Taking additional actions after submitting the login form](%{documentation_link})."
@ -38104,9 +38104,6 @@ msgstr ""
msgid "MemberRole|Direct users assigned"
msgstr ""
msgid "MemberRole|Dismiss Planner role promotion"
msgstr ""
msgid "MemberRole|Edit admin role"
msgstr ""
@ -38158,9 +38155,6 @@ msgstr ""
msgid "MemberRole|Learn more about %{linkStart}available custom permissions%{linkEnd}."
msgstr ""
msgid "MemberRole|Learn more about roles and permissions"
msgstr ""
msgid "MemberRole|Manage roles"
msgstr ""
@ -38173,9 +38167,6 @@ msgstr ""
msgid "MemberRole|Name"
msgstr ""
msgid "MemberRole|New Planner role"
msgstr ""
msgid "MemberRole|New role"
msgstr ""
@ -38281,12 +38272,6 @@ msgstr ""
msgid "MemberRole|The Owner role is typically assigned to the individual or team responsible for managing and maintaining the group or creating the project. This role has the highest level of administrative control, and can manage all aspects of the group or project, including managing other Owners."
msgstr ""
msgid "MemberRole|The Planner role is a hybrid of the existing Guest and Reporter roles but designed for users who need access to planning workflows."
msgstr ""
msgid "MemberRole|The Planner role is a hybrid of the existing Guest and Reporter roles but designed for users who need access to planning workflows. For more information about the new role, see %{blogLinkStart}our blog%{blogLinkEnd} or %{learnMoreStart}learn more about roles and permissions%{learnMoreEnd}."
msgstr ""
msgid "MemberRole|The Planner role is suitable for team members who need to manage projects and track work items but do not need to contribute code, such as project managers and scrum masters."
msgstr ""
@ -53238,6 +53223,9 @@ msgstr ""
msgid "Runners|No project runners found"
msgstr ""
msgid "Runners|No runners managers found"
msgstr ""
msgid "Runners|No spot. Default choice for Windows Shell executor."
msgstr ""
@ -53419,6 +53407,9 @@ msgstr ""
msgid "Runners|Runner"
msgstr ""
msgid "Runners|Runner %{name}"
msgstr ""
msgid "Runners|Runner %{name} was assigned to this project."
msgstr ""
@ -53485,6 +53476,9 @@ msgstr ""
msgid "Runners|Runner is stale; last contact was %{runner_contact} ago"
msgstr ""
msgid "Runners|Runner managers registered under this configuration are listed here. Register and start at least one runner manager."
msgstr ""
msgid "Runners|Runner performance insights"
msgstr ""

View File

@ -104,7 +104,6 @@ spec/frontend/projects/settings/components/branch_rule_modal_spec.js
spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
spec/frontend/ref/init_ambiguous_ref_modal_spec.js
spec/frontend/releases/components/asset_links_form_spec.js
spec/frontend/repository/components/table/index_spec.js
spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
spec/frontend/sidebar/components/confidential/confidentiality_dropdown_spec.js
spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js

View File

@ -11,6 +11,7 @@ import RunnerGroups from '~/ci/runner/components/runner_groups.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import RunnerTag from '~/ci/runner/components/runner_tag.vue';
import RunnerManagers from '~/ci/runner/components/runner_managers.vue';
import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
@ -26,12 +27,14 @@ describe('RunnerDetails', () => {
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
const findRunnerManagers = () => wrapper.findComponent(RunnerManagers);
const findRunnerJobs = () => wrapper.findComponent(RunnerJobs);
const findDdContent = (label) => findDd(label, wrapper).text().replace(/\s+/g, ' ');
const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerDetails, {
propsData: {
runnerId: mockRunner.id,
...props,
},
stubs: {
@ -113,8 +116,8 @@ describe('RunnerDetails', () => {
});
});
describe('"Runners" field', () => {
it('displays runner managers count of $count', () => {
describe('Status', () => {
it('displays runner managers', () => {
createComponent({
props: {
runner: mockRunner,
@ -123,6 +126,18 @@ describe('RunnerDetails', () => {
expect(findRunnerManagers().props('runner')).toEqual(mockRunner);
});
it('displays runner jobs', () => {
createComponent({
props: {
runner: mockRunner,
showAccessHelp: true,
},
});
expect(findRunnerJobs().props('runnerId')).toEqual(mockRunner.id);
expect(findRunnerJobs().props('showAccessHelp')).toEqual(true);
});
});
describe('Group runners', () => {

View File

@ -75,7 +75,7 @@ describe('RunnerHeader', () => {
},
});
expect(findPageHeading().props('heading')).toBe(`#99 (${mockRunnerSha})`);
expect(findPageHeading().props('heading')).toBe(`Runner #99 (${mockRunnerSha})`);
});
it('displays the runner locked icon', () => {

View File

@ -1,4 +1,4 @@
import { GlTableLite } from '@gitlab/ui';
import { GlTableLite, GlEmptyState } from '@gitlab/ui';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@ -20,6 +20,7 @@ describe('RunnerJobs', () => {
const findCrudComponent = () => wrapper.findComponent(CrudComponent);
const findCrudExpandToggle = () => wrapper.findByTestId('crud-collapse-toggle');
const findRunnerManagersTable = () => wrapper.findComponent(RunnerManagersTable);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerManagers, {
@ -34,7 +35,7 @@ describe('RunnerJobs', () => {
});
};
it('hides if no runners', () => {
it('shows an empty state if no runners', () => {
createComponent({
props: {
runner: {
@ -46,7 +47,12 @@ describe('RunnerJobs', () => {
},
});
expect(findCrudComponent().exists()).toBe(false);
expect(findEmptyState().props('title')).toBe('No runners managers found');
expect(findEmptyState().text()).toBe(
'Runner managers registered under this configuration are listed here. Register and start at least one runner manager.',
);
expect(findRunnerManagersTable().exists()).toBe(false);
});
describe('Runners count', () => {

View File

@ -15,7 +15,6 @@ import { captureException } from '~/ci/runner/sentry_utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
import RunnerShow from '~/ci/runner/components/runner_show.vue';
@ -41,7 +40,6 @@ describe('RunnerShow', () => {
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerHeaderActions = () => wrapper.findComponent(RunnerHeaderActions);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerJobs = () => wrapper.findComponent(RunnerJobs);
const createComponent = ({ props = {} } = {}) => {
mockApollo = createMockApollo([[runnerQuery, runnerQueryHandler]]);
@ -134,10 +132,4 @@ describe('RunnerShow', () => {
expect(findRunnerDetails().props('runner')).toEqual(mockRunner);
});
it('shows runner jobs', () => {
createComponent();
expect(findRunnerJobs().props('runnerId')).toBe(mockRunnerId);
});
});

View File

@ -1,11 +1,7 @@
import { GlCollapsibleListbox, GlBadge, GlPopover } from '@gitlab/ui';
import { GlCollapsibleListbox } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { roleDropdownItems } from '~/members/utils';
import RoleSelector from '~/members/components/role_selector.vue';
import {
ACCESS_LEVEL_PLANNER_STRING,
ACCESS_LEVEL_MAINTAINER_STRING,
} from '~/access_level/constants';
import { member } from '../mock_data';
describe('Role selector', () => {
@ -25,7 +21,6 @@ describe('Role selector', () => {
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const getDropdownItem = (id) => wrapper.findByTestId(`listbox-item-${id}`);
const findRoleName = (id) => getDropdownItem(id).find('[data-testid="role-name"]');
const findGlPopover = () => wrapper.findComponent(GlPopover);
describe('dropdown component', () => {
it('shows the dropdown with the expected props', () => {
@ -74,21 +69,5 @@ describe('Role selector', () => {
it.each(dropdownItems.flatten)('shows the role name for $text', ({ value, text }) => {
expect(findRoleName(value).text()).toBe(text);
});
it('shows badge only on `Planner role` with a popover', () => {
// Show on planner dropdown item
expect(getDropdownItem(ACCESS_LEVEL_PLANNER_STRING).findComponent(GlBadge).exists()).toBe(
true,
);
// Do not show on maintainer or any other dropdown item
expect(getDropdownItem(ACCESS_LEVEL_MAINTAINER_STRING).findComponent(GlBadge).exists()).toBe(
false,
);
expect(findGlPopover().text()).toBe(
'The Planner role is a hybrid of the existing Guest and Reporter roles but designed for users who need access to planning workflows.',
);
});
});
});

View File

@ -1,38 +0,0 @@
import { GlBanner, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import PlanRoleBanner from '~/planner_role_banner/components/planner_role_banner.vue';
describe('Planner role banner', () => {
let wrapper;
const userCalloutDismissSpy = jest.fn();
const createWrapper = (shouldShowCallout) => {
wrapper = shallowMount(PlanRoleBanner, {
stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss: userCalloutDismissSpy,
shouldShowCallout,
}),
},
});
};
const findGlBanner = () => wrapper.findComponent(GlBanner);
const findGlSprintf = () => wrapper.findComponent(GlSprintf);
it('renders the banner', () => {
createWrapper(true);
expect(findGlBanner().props('title')).toBe('New Planner role');
expect(findGlSprintf().attributes('message')).toBe(
'The Planner role is a hybrid of the existing Guest and Reporter roles but designed for users who need access to planning workflows. For more information about the new role, see %{blogLinkStart}our blog%{blogLinkEnd} or %{learnMoreStart}learn more about roles and permissions%{learnMoreEnd}.',
);
});
it('does not render the banner', () => {
createWrapper(false);
expect(findGlBanner().exists()).toBe(false);
});
});

View File

@ -100,6 +100,11 @@ function factory({
commits,
},
apolloProvider: createMockApolloProvider(ref),
data() {
return {
ref,
};
},
});
}

View File

@ -46,7 +46,7 @@ describe('TooltipOnTruncate directive', () => {
});
it('unbinds when destroyed', () => {
wrapper.vm.$destroy();
wrapper.destroy();
expect(getBinding(wrapper.element, 'gl-tooltip')).toBeUndefined();
expect(getBinding(wrapper.element, 'gl-resize-observer')).toBeUndefined();

View File

@ -0,0 +1,333 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::CollationChecker, feature_category: :database do
describe '.run' do
let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
let(:database_name) { 'main' }
let(:logger) { instance_double(Gitlab::AppLogger, info: nil, warn: nil, error: nil) }
it 'instantiates the class and calls run' do
instance = instance_double(described_class)
expect(Gitlab::Database::EachDatabase).to receive(:each_connection)
.with(only: database_name)
.and_yield(connection, database_name)
expect(described_class).to receive(:new)
.with(connection, database_name, logger)
.and_return(instance)
expect(instance).to receive(:run)
described_class.run(database_name: database_name, logger: logger)
end
end
describe '#run' do
# Mock-based tests for edge cases and error handling
context 'with mocked database connection' do
let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
let(:database_name) { 'main' }
let(:logger) { instance_double(Gitlab::AppLogger, info: nil, warn: nil, error: nil) }
let(:checker) { described_class.new(connection, database_name, logger) }
context 'when no collation mismatches are found' do
let(:empty_results) { instance_double(ActiveRecord::Result, to_a: []) }
before do
allow(connection).to receive(:select_all)
.with(described_class::COLLATION_VERSION_MISMATCH_QUERY)
.and_return(empty_results)
end
it 'logs a success message and returns no mismatches' do
expect(logger).to receive(:info).with("Checking for PostgreSQL collation mismatches on main database...")
expect(logger).to receive(:info).with("No collation mismatches detected on main.")
result = checker.run
expect(result).to eq({ mismatches_found: false, affected_indexes: [] })
end
end
context 'when collation mismatches exist but no indexes are affected' do
let(:mismatches) do
instance_double(
ActiveRecord::Result,
to_a: [{ 'collation_name' => 'en_US.utf8', 'stored_version' => '1.2.3', 'actual_version' => '1.2.4' }]
)
end
let(:empty_affected) { instance_double(ActiveRecord::Result, to_a: []) }
before do
allow(connection).to receive(:quote)
.with('en_US.utf8')
.and_return("'en_US.utf8'")
allow(connection).to receive(:select_all)
.with(described_class::COLLATION_VERSION_MISMATCH_QUERY)
.and_return(mismatches)
allow(connection).to receive(:select_all)
.with(/SELECT DISTINCT.*FROM.*pg_collation.*WHERE.*collname IN \('en_US.utf8'\)/m)
.and_return(empty_affected)
end
it 'logs warnings about mismatches but reports no affected indexes' do
expect(logger).to receive(:info).with("Checking for PostgreSQL collation mismatches on main database...")
expect(logger).to receive(:warn).with("⚠️ COLLATION MISMATCHES DETECTED on main database!")
expect(logger).to receive(:warn).with("1 collation(s) have version mismatches:")
expect(logger).to receive(:warn).with(" - en_US.utf8: stored=1.2.3, actual=1.2.4")
expect(logger).to receive(:info).with("No indexes appear to be affected by the collation mismatches.")
result = checker.run
expect(result).to eq({ mismatches_found: true, affected_indexes: [] })
end
end
context 'when collation mismatches exist and indexes are affected (mock version)' do
let(:mismatches) do
instance_double(
ActiveRecord::Result,
to_a: [{ 'collation_name' => 'en_US.utf8', 'stored_version' => '1.2.3', 'actual_version' => '1.2.4' }]
)
end
let(:affected_indexes) do
instance_double(
ActiveRecord::Result,
to_a: [
{
'table_name' => 'projects',
'index_name' => 'index_projects_on_name',
'affected_columns' => 'name',
'index_type' => 'btree',
'is_unique' => 't'
},
{
'table_name' => 'users',
'index_name' => 'index_users_on_username',
'affected_columns' => 'username',
'index_type' => 'btree',
'is_unique' => 'f'
}
]
)
end
before do
allow(connection).to receive(:select_all)
.with(described_class::COLLATION_VERSION_MISMATCH_QUERY)
.and_return(mismatches)
allow(connection).to receive(:quote)
.with('en_US.utf8')
.and_return("'en_US.utf8'")
allow(connection).to receive(:select_all)
.with(/SELECT DISTINCT.*FROM.*pg_collation.*WHERE.*collname IN \('en_US.utf8'\)/m)
.and_return(affected_indexes)
end
it 'logs warnings and provides remediation guidance' do
# Test basic detection
expect(logger).to receive(:info).with("Checking for PostgreSQL collation mismatches on main database...")
expect(logger).to receive(:warn).with("⚠️ COLLATION MISMATCHES DETECTED on main database!")
expect(logger).to receive(:warn).with("1 collation(s) have version mismatches:")
expect(logger).to receive(:warn).with(" - en_US.utf8: stored=1.2.3, actual=1.2.4")
# Test affected indexes are listed
expect(logger).to receive(:warn).with("Affected indexes that need to be rebuilt:")
expect(logger).to receive(:warn).with(" - index_projects_on_name (btree) on table projects")
expect(logger).to receive(:warn).with(" • Affected columns: name")
expect(logger).to receive(:warn).with(" • Type: UNIQUE")
expect(logger).to receive(:warn).with(" - index_users_on_username (btree) on table users")
expect(logger).to receive(:warn).with(" • Affected columns: username")
expect(logger).to receive(:warn).with(" • Type: NON-UNIQUE")
# Test remediation header
expect(logger).to receive(:warn).with("\nREMEDIATION STEPS:")
expect(logger).to receive(:warn).with("1. Put GitLab into maintenance mode")
expect(logger).to receive(:warn).with("2. Run the following SQL commands:")
# Test duplicate entry checks
expect(logger).to receive(:warn).with("\n# Step 1: Check for duplicate entries in unique indexes")
expect(logger).to receive(:warn).with(
"-- Check for duplicates in projects (unique index: index_projects_on_name)"
)
expect(logger).to receive(:warn).with(
/SELECT name, COUNT\(\*\), ARRAY_AGG\(id\) FROM projects GROUP BY name HAVING COUNT\(\*\) > 1 LIMIT 1;/
)
expect(logger).to receive(:warn).with(/\n# If duplicates exist/)
# Test index rebuild commands
expect(logger).to receive(:warn).with("\n# Step 2: Rebuild affected indexes")
expect(logger).to receive(:warn).with("# Option A: Rebuild individual indexes with minimal downtime:")
expect(logger).to receive(:warn).with("REINDEX INDEX index_projects_on_name CONCURRENTLY;")
expect(logger).to receive(:warn).with("REINDEX INDEX index_users_on_username CONCURRENTLY;")
expect(logger).to receive(:warn).with(
"\n# Option B: Alternatively, rebuild all indexes at once (requires downtime):"
)
expect(logger).to receive(:warn).with("REINDEX DATABASE main;")
# Test collation refresh commands
expect(logger).to receive(:warn).with("\n# Step 3: Refresh collation versions")
expect(logger).to receive(:warn).with("ALTER DATABASE main REFRESH COLLATION VERSION;")
expect(logger).to receive(:warn).with(
"-- This updates all collation versions in the database to match the current OS"
)
# Test conclusion
expect(logger).to receive(:warn).with("\n3. Take GitLab out of maintenance mode")
expect(logger).to receive(:warn).with("\nFor more information, see: https://docs.gitlab.com/administration/postgresql/upgrading_os/")
result = checker.run
expect(result).to include(mismatches_found: true)
expect(result[:affected_indexes]).to eq(affected_indexes.to_a)
end
end
context 'when there is an error checking for mismatches' do
before do
allow(connection).to receive(:select_all)
.with(described_class::COLLATION_VERSION_MISMATCH_QUERY)
.and_raise(ActiveRecord::StatementInvalid, 'test error')
end
it 'logs the error and returns no mismatches' do
expect(logger).to receive(:info).with("Checking for PostgreSQL collation mismatches on main database...")
expect(logger).to receive(:error).with("Error checking collation mismatches: test error")
result = checker.run
expect(result).to eq({ mismatches_found: false, affected_indexes: [] })
end
end
context 'when there is an error finding affected indexes' do
let(:mismatches) do
instance_double(
ActiveRecord::Result,
to_a: [{ 'collation_name' => 'en_US.utf8', 'stored_version' => '1.2.3', 'actual_version' => '1.2.4' }]
)
end
before do
allow(connection).to receive(:select_all)
.with(described_class::COLLATION_VERSION_MISMATCH_QUERY)
.and_return(mismatches)
allow(connection).to receive(:quote)
.with('en_US.utf8')
.and_return("'en_US.utf8'")
allow(connection).to receive(:select_all)
.with(/SELECT DISTINCT.*FROM.*pg_collation.*WHERE.*collname IN \('en_US.utf8'\)/m)
.and_raise(ActiveRecord::StatementInvalid, 'test error')
end
it 'logs the error and returns only mismatches' do
expect(logger).to receive(:info).with("Checking for PostgreSQL collation mismatches on main database...")
expect(logger).to receive(:warn).with("⚠️ COLLATION MISMATCHES DETECTED on main database!")
expect(logger).to receive(:warn).with("1 collation(s) have version mismatches:")
expect(logger).to receive(:warn).with(" - en_US.utf8: stored=1.2.3, actual=1.2.4")
expect(logger).to receive(:error).with("Error finding affected indexes: test error")
result = checker.run
expect(result).to include(mismatches_found: true)
expect(result[:affected_indexes]).to eq([])
end
end
end
# Real database test for the happy path
context 'with real database connection' do
let(:connection) { ActiveRecord::Base.connection }
let(:database_name) { connection.current_database }
let(:logger) { instance_double(Logger, info: nil, warn: nil, error: nil) }
let(:checker) { described_class.new(connection, database_name, logger) }
let(:table_name) { '_test_c_collation_table' }
let(:index_name) { '_test_c_collation_index' }
let(:c_collation) { 'C' } # Use standard C collation which should be available
# Find the real OID of the C collation for our test
let!(:c_collation_info) do
connection.select_all(
"SELECT oid FROM pg_collation WHERE collname = '#{c_collation}' AND collprovider = 'c' LIMIT 1"
).first
end
let!(:c_collation_oid) { c_collation_info&.[]('oid') }
before do
skip 'C collation not found in database' unless c_collation_info
# Create test table with a column using C collation
connection.execute(<<~SQL)
CREATE TABLE #{table_name} (
id serial PRIMARY KEY,
test_col varchar(255) COLLATE "#{c_collation}" NOT NULL
);
SQL
# Create an index on the collated column
connection.execute(<<~SQL)
CREATE INDEX #{index_name} ON #{table_name} (test_col);
SQL
# Insert test data
connection.execute(<<~SQL)
INSERT INTO #{table_name} (test_col) VALUES ('value1');
SQL
end
after do
connection.execute("DROP TABLE IF EXISTS #{table_name} CASCADE;")
end
it 'detects C collation mismatch and finds affected index' do
allow(checker).to receive(:mismatched_collations) do
# Create a modified query to simulate actual version being different
modified_query = described_class::COLLATION_VERSION_MISMATCH_QUERY
.gsub('collversion', "'123.456'")
.gsub('pg_collation_actual_version(oid)', "'987.654.321'")
connection.select_all(modified_query).to_a
end
# Run the checker with our mocked version mismatch
result = checker.run
# Verify we found mismatches
expect(result[:mismatches_found]).to be true
# Verify we found affected indexes
expect(result[:affected_indexes]).not_to be_empty
# Verify we found our test table index
test_indexes = result[:affected_indexes].select { |idx| idx['table_name'] == table_name }
expect(test_indexes).not_to be_empty, "Expected to find test table index but found none"
expect(test_indexes.first['index_name']).to eq(index_name), "Expected to find our specific test index"
# Verify remediation SQL includes our test index
rebuild_commands = []
allow(logger).to receive(:warn) do |message|
rebuild_commands << message if message.include?('REINDEX INDEX')
end
# Run again to capture remediation SQL
checker.run
# Verify rebuild command for our test index
expect(rebuild_commands.any? { |cmd| cmd.include?(index_name) }).to be true
end
end
end
end

View File

@ -12,7 +12,6 @@ RSpec.describe 'Bulk update work items', feature_category: :team_planning do
let_it_be(:label2) { create(:group_label, group: group) }
let_it_be_with_reload(:updatable_work_items) { create_list(:work_item, 2, project: project, label_ids: [label1.id]) }
let_it_be(:private_project) { create(:project, :private) }
let(:parent) { project }
let(:mutation) { graphql_mutation(:work_item_bulk_update, base_arguments.merge(additional_arguments)) }
let(:mutation_response) { graphql_mutation_response(:work_item_bulk_update) }
@ -120,4 +119,162 @@ RSpec.describe 'Bulk update work items', feature_category: :team_planning do
)
end
end
context 'when updating confidential attribute' do
let(:additional_arguments) do
{
'confidential' => true
}
end
it 'updates the confidential attribute for all work items' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
updatable_work_items.each(&:reload)
end.to change { updatable_work_items.map(&:confidential) }.from([false, false]).to([true, true])
expect(mutation_response).to include(
'updatedWorkItemCount' => updatable_work_items.count
)
end
end
context 'when updating multiple attributes simultaneously' do
let_it_be(:assignee) { create(:user, developer_of: group) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:additional_arguments) do
{
'confidential' => true,
'assigneesWidget' => {
'assigneeIds' => [assignee.to_gid.to_s]
},
'milestoneWidget' => {
'milestoneId' => milestone.to_gid.to_s
},
'labelsWidget' => {
'addLabelIds' => [label2.to_gid.to_s]
}
}
end
it 'updates all specified attributes' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
updatable_work_items.each(&:reload)
end.to change { updatable_work_items.map(&:confidential) }.from([false, false]).to([true, true])
.and change { updatable_work_items.flat_map(&:assignee_ids) }.from([]).to([assignee.id] * 2)
.and change { updatable_work_items.map(&:milestone_id) }.from([nil, nil]).to([milestone.id] * 2)
.and change { updatable_work_items.flat_map(&:label_ids) }.from([label1.id] * 2).to([label1.id, label2.id] * 2)
expect(mutation_response).to include(
'updatedWorkItemCount' => updatable_work_items.count
)
end
context 'when updating work items that do not support requested widgets' do
let_it_be(:key_result) { create(:work_item, :key_result, project: project) }
let_it_be(:issue) { create(:work_item, :issue, project: project) }
let(:updatable_work_item_ids) { [key_result.to_gid.to_s, issue.to_gid.to_s] }
context 'when updating milestone widget' do
let(:additional_arguments) do
{
'milestoneWidget' => {
'milestoneId' => milestone.to_gid.to_s
}
}
end
it 'updates only work items that support the milestone widget' do
# Key Results don't support milestones, but Issues do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { issue.reload.milestone }.from(nil).to(milestone)
.and not_change { key_result.reload.attributes['milestone_id'] }
expect(mutation_response).to include(
'updatedWorkItemCount' => 1
)
end
end
end
end
context 'when work items have different types' do
let_it_be(:task) { create(:work_item, :task, project: project) }
let_it_be(:issue) { create(:work_item, :issue, project: project) }
let(:updatable_work_item_ids) { [task.to_gid.to_s, issue.to_gid.to_s] }
let(:additional_arguments) do
{
'labelsWidget' => {
'addLabelIds' => [label2.to_gid.to_s]
}
}
end
it 'updates work items of different types' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
task.reload
issue.reload
end.to change { task.label_ids + issue.label_ids }.from([]).to([label2.id, label2.id])
expect(mutation_response).to include(
'updatedWorkItemCount' => 2
)
end
end
context 'when some updates fail' do
let_it_be(:work_item_with_validation) { create(:work_item, project: project) }
let(:updatable_work_item_ids) do
updatable_work_items.map do |i|
i.to_gid.to_s
end + [work_item_with_validation.to_gid.to_s]
end
let(:additional_arguments) do
{
'confidential' => true
}
end
before do
# Simulate a validation error for one work item
allow_next_instance_of(WorkItems::UpdateService) do |service|
allow(service).to receive(:execute).and_call_original
allow(service).to receive(:execute).with(work_item_with_validation).and_return(
{ status: :error, message: 'Validation failed' }
)
end
end
it 'updates only the successful work items' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
updatable_work_items.each(&:reload)
work_item_with_validation.reload
end.to change { updatable_work_items.map(&:confidential) }.from([false, false]).to([true, true])
.and not_change { work_item_with_validation.confidential }.from(false)
expect(mutation_response).to include(
'updatedWorkItemCount' => updatable_work_items.count
)
end
end
context 'when no work items are provided' do
let(:updatable_work_item_ids) { [] }
it 'returns 0 updated work items' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to include(
'updatedWorkItemCount' => 0
)
end
end
end

View File

@ -13,6 +13,8 @@ RSpec.describe WorkItems::BulkUpdateService, feature_category: :team_planning do
let_it_be(:label1) { create(:group_label, group: parent_group) }
let_it_be(:label2) { create(:group_label, group: parent_group) }
let_it_be(:label3) { create(:group_label, group: private_group) }
let_it_be(:assignee) { create(:user, developer_of: group) }
let_it_be(:milestone) { create(:milestone, group: group) }
let_it_be_with_reload(:work_item1) { create(:work_item, :group_level, namespace: group, labels: [label1]) }
let_it_be_with_reload(:work_item2) { create(:work_item, project: project, labels: [label1]) }
let_it_be_with_reload(:work_item3) { create(:work_item, :group_level, namespace: parent_group, labels: [label1]) }
@ -21,7 +23,7 @@ RSpec.describe WorkItems::BulkUpdateService, feature_category: :team_planning do
let(:updatable_work_items) { [work_item1, work_item2, work_item3, work_item4] }
let(:updatable_work_item_ids) { updatable_work_items.map(&:id) }
let(:widget_params) do
let(:attributes) do
{
labels_widget: {
add_label_ids: [label2.id],
@ -35,7 +37,7 @@ RSpec.describe WorkItems::BulkUpdateService, feature_category: :team_planning do
parent: parent,
current_user: current_user,
work_item_ids: updatable_work_item_ids,
widget_params: widget_params
attributes: attributes
).execute
end
@ -62,6 +64,42 @@ RSpec.describe WorkItems::BulkUpdateService, feature_category: :team_planning do
expect(service_result[:updated_work_item_count]).to eq(1)
end
context 'when updating non-widget attributes' do
let(:attributes) { { confidential: true } }
it 'updates confidential attribute' do
expect do
service_result
end.to change { work_item2.reload.confidential }.from(false).to(true)
end
end
context 'when updating multiple attributes' do
let(:attributes) do
{
confidential: true,
assignees_widget: {
assignee_ids: [assignee.id]
},
milestone_widget: {
milestone_id: milestone.id
},
labels_widget: {
add_label_ids: [label2.id]
}
}
end
it 'updates all attributes correctly' do
expect do
service_result
end.to change { work_item2.reload.confidential }.from(false).to(true)
.and change { work_item2.assignee_ids }.from([]).to([assignee.id])
.and change { work_item2.milestone_id }.from(nil).to(milestone.id)
.and change { work_item2.label_ids }.from([label1.id]).to([label1.id, label2.id])
end
end
context 'with EE license', if: Gitlab.ee? do
before do
stub_licensed_features(epics: true)

View File

@ -575,6 +575,72 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor
end
end
describe 'collation_checker' do
context 'with a single database' do
before do
skip_if_multiple_databases_are_setup
end
it 'calls Gitlab::Database::CollationChecker with correct arguments' do
logger_double = instance_double(Logger, level: nil, info: nil, warn: nil, error: nil)
allow(Logger).to receive(:new).with($stdout).and_return(logger_double)
expect(Gitlab::Database::CollationChecker).to receive(:run)
.with(logger: logger_double)
run_rake_task('gitlab:db:collation_checker')
end
end
context 'with multiple databases' do
let(:logger_double) { instance_double(Logger, level: nil, info: nil, warn: nil, error: nil) }
before do
skip_if_multiple_databases_not_setup(:ci)
allow(Logger).to receive(:new).with($stdout).and_return(logger_double)
end
it 'calls Gitlab::Database::CollationChecker with correct arguments' do
expect(Gitlab::Database::CollationChecker).to receive(:run)
.with(logger: logger_double)
run_rake_task('gitlab:db:collation_checker')
end
context 'when the single database task is used' do
before do
skip_if_shared_database(:ci)
end
it 'calls Gitlab::Database::CollationChecker with the main database' do
expect(Gitlab::Database::CollationChecker).to receive(:run)
.with(database_name: 'main', logger: logger_double)
run_rake_task('gitlab:db:collation_checker:main')
end
it 'calls Gitlab::Database::CollationChecker with the ci database' do
expect(Gitlab::Database::CollationChecker).to receive(:run)
.with(database_name: 'ci', logger: logger_double)
run_rake_task('gitlab:db:collation_checker:ci')
end
end
context 'with geo configured' do
before do
skip_unless_geo_configured
end
it 'does not create a task for the geo database' do
expect { run_rake_task('gitlab:db:collation_checker:geo') }
.to raise_error(/Don't know how to build task 'gitlab:db:collation_checker:geo'/)
end
end
end
end
describe 'dictionary generate' do
let(:db_config) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, name: 'fake_db') }

View File

@ -22,6 +22,7 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker, feature_category: :sourc
context 'with a non-existing merge request' do
it 'does nothing' do
expect(::MergeRequests::RetargetChainService).not_to receive(:new)
expect(::Projects::DeleteBranchWorker).not_to receive(:new)
worker.perform(non_existing_record_id, sha, user.id)
@ -30,6 +31,7 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker, feature_category: :sourc
context 'with a non-existing user' do
it 'does nothing' do
expect(::MergeRequests::RetargetChainService).not_to receive(:new)
expect(::Projects::DeleteBranchWorker).not_to receive(:new)
worker.perform(merge_request.id, sha, non_existing_record_id)