From 1bf957f0b227f95a52e737b4e671c235c824bd75 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 15 May 2025 21:12:45 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- GITLAB_PAGES_VERSION | 2 +- .../code_dropdown/code_dropdown.vue | 15 + data/whats_new/202505150001_18_0.yml | 100 ++++++ ...incomplete_external_audit_destinations.yml | 8 + ...e_instance_external_audit_destinations.yml | 8 + ...external_group_audit_event_destinations.rb | 2 +- ...al_group_audit_event_destinations_fixed.rb | 15 +- ...instance_audit_event_destinations_fixed.rb | 16 +- ..._incomplete_external_audit_destinations.rb | 25 ++ ...te_instance_external_audit_destinations.rb | 25 ++ db/schema_migrations/20250429171748 | 1 + db/schema_migrations/20250429171801 | 1 + doc/api/graphql/reference/_index.md | 2 + ...ompliance_standards_adherence_dashboard.md | 13 +- ..._incomplete_external_audit_destinations.rb | 14 + ...te_instance_external_audit_destinations.rb | 14 + lib/gitlab/fp/result.rb | 173 ++++++++- .../code_dropdown/code_dropdown_spec.js | 1 + spec/lib/gitlab/fp/result_spec.rb | 332 +++++++++++++++++- ...oup_audit_event_destinations_fixed_spec.rb | 28 +- ...nce_audit_event_destinations_fixed_spec.rb | 28 +- ...mplete_external_audit_destinations_spec.rb | 37 ++ ...stance_external_audit_destinations_spec.rb | 37 ++ spec/support/matchers/invoke_rop_steps.rb | 12 +- 24 files changed, 806 insertions(+), 103 deletions(-) create mode 100644 data/whats_new/202505150001_18_0.yml create mode 100644 db/docs/batched_background_migrations/fix_incomplete_external_audit_destinations.yml create mode 100644 db/docs/batched_background_migrations/fix_incomplete_instance_external_audit_destinations.yml create mode 100644 db/post_migrate/20250429171748_queue_fix_incomplete_external_audit_destinations.rb create mode 100644 db/post_migrate/20250429171801_queue_fix_incomplete_instance_external_audit_destinations.rb create mode 100644 db/schema_migrations/20250429171748 create mode 100644 db/schema_migrations/20250429171801 create mode 100644 lib/gitlab/background_migration/fix_incomplete_external_audit_destinations.rb create mode 100644 lib/gitlab/background_migration/fix_incomplete_instance_external_audit_destinations.rb create mode 100644 spec/migrations/20250429171748_queue_fix_incomplete_external_audit_destinations_spec.rb create mode 100644 spec/migrations/20250429171801_queue_fix_incomplete_instance_external_audit_destinations_spec.rb diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 67ebf0f7ff6..1966080cbf6 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -5ce562f1608201580a29260861728a6e0a9bd087 +f32d3d9029112f6740e784a031d4b9b60f49aa48 diff --git a/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue b/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue index 96a04d5c532..ac2b9744fe1 100644 --- a/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue @@ -61,22 +61,37 @@ export default { Boolean(this.sshUrl) && { text: __('Visual Studio Code (SSH)'), href: `${this.$options.vsCodeBaseUrl}${this.sshUrlEncoded}`, + extraAttrs: { + isUnsafeLink: true, + }, }, Boolean(this.httpUrl) && { text: __('Visual Studio Code (HTTPS)'), href: `${this.$options.vsCodeBaseUrl}${this.httpUrlEncoded}`, + extraAttrs: { + isUnsafeLink: true, + }, }, Boolean(this.sshUrl) && { text: __('IntelliJ IDEA (SSH)'), href: `${this.$options.jetBrainsBaseUrl}${this.sshUrlEncoded}`, + extraAttrs: { + isUnsafeLink: true, + }, }, Boolean(this.httpUrl) && { text: __('IntelliJ IDEA (HTTPS)'), href: `${this.$options.jetBrainsBaseUrl}${this.httpUrlEncoded}`, + extraAttrs: { + isUnsafeLink: true, + }, }, Boolean(this.xcodeUrl) && { text: __('Xcode'), href: this.xcodeUrl, + extraAttrs: { + isUnsafeLink: true, + }, }, ].filter(Boolean); diff --git a/data/whats_new/202505150001_18_0.yml b/data/whats_new/202505150001_18_0.yml new file mode 100644 index 00000000000..ed89dea37ec --- /dev/null +++ b/data/whats_new/202505150001_18_0.yml @@ -0,0 +1,100 @@ +- name: "GitLab Premium and Ultimate with Duo" + description: | + We're excited to announce GitLab Premium with Duo and GitLab Ultimate with Duo. GitLab Premium and Ultimate now include AI-native features. + + GitLab's AI-native features include Code Suggestions and Chat within the IDE. Development teams can use these features to: + + - Analyze, understand, and explain code + - Write secure code faster + - Quickly generate tests to maintain code quality + - Easily refactor code to improve performance or use specific libraries + stage: ai-powered + self-managed: true + gitlab-com: true + available_in: [Premium, Ultimate] + documentation_link: 'https://docs.gitlab.com/user/gitlab_duo/#summary-of-gitlab-duo-features' + image_url: https://about.gitlab.com/images/18_0/Premium_Duo.png + published_at: 2025-05-15 + release: 18.0 +- name: "Automatic reviews with Duo Code Review" + description: | + Duo Code Review provides valuable insights during the review process, but currently requires you to manually request reviews on each merge request. + + You can now configure GitLab Duo Code Review to run automatically on merge requests by updating your project's merge request settings. When enabled, Duo Code Review automatically reviews merge requests unless: + + - The merge request is marked as draft. + - The merge request contains no changes. + + Automatic reviews ensure that all code in your project receives a review, consistently improving code quality across your codebase. + stage: create + self-managed: true + gitlab-com: true + available_in: [Premium, Ultimate] + documentation_link: 'https://docs.gitlab.com/user/project/merge_requests/duo_in_merge_requests/#automatic-reviews-from-gitlab-duo' + image_url: https://about.gitlab.com/images/18_0/create-auto-dcr.png + published_at: 2025-05-15 + release: 18.0 +- name: "GitLab Query Language views enhancements" + description: | + We've made significant improvements to GitLab Query Language (GLQL) views. These improvements include support for: + + - The `>=` and `<=` operators for all date types + - The **View actions** dropdown in views + - The **Reload** action + - Field aliases + - Aliasing columns to a custom name in GLQL tables + + We welcome your feedback on this enhancement, and on GLQL views in general, in [issue 509791](https://gitlab.com/gitlab-org/gitlab/-/issues/509791). + stage: plan + self-managed: true + gitlab-com: true + available_in: [Free, Premium, Ultimate] + documentation_link: 'https://docs.gitlab.com/user/glql/' + published_at: 2025-05-15 + release: 18.0 +- name: "New CI/CD analytics view for projects in limited availability" + description: | + The redesigned CI/CD analytics view transforms how your development teams analyze, monitor, and optimize pipeline performance + and reliability. Developers can access intuitive visualizations in the GitLab UI that reveal performance + trends and reliability metrics. Embedding these insights in your project repository eliminates context-switching + that disrupts developer flow. Teams can identify and address pipeline bottlenecks that drain productivity. + This enhancement leads to faster development cycles, improved collaboration, and data-driven confidence to optimize your + CI/CD workflows in GitLab. + stage: verify + self-managed: true + gitlab-com: true + available_in: [Free, Premium, Ultimate] + documentation_link: 'https://docs.gitlab.com/user/analytics/ci_cd_analytics/' + image_url: https://img.youtube.com/vi/78Nxbem9OAk/hqdefault.jpg + published_at: 2025-05-15 + release: 18.0 +- name: "Shared Kubernetes namespace for workspaces" + description: | + You can now create GitLab workspaces in a shared Kubernetes namespace. This removes the need to create + a new namespace for every workspace and eliminates the requirement to give elevated ClusterRole + permission to the agent. With this feature, you can more easily adopt workspaces in secure or + restricted environments, offering a simpler path to scale. + + To enable shared namespaces, set the `shared_namespace` field in your agent configuration file to + specify the Kubernetes namespace you want to use for all workspaces. + + Thank you to the half dozen community contributors who helped build this feature through + [GitLab's Co-Create program](https://about.gitlab.com/community/co-create/)! + stage: create + self-managed: true + gitlab-com: true + available_in: [Premium, Ultimate] + documentation_link: 'https://docs.gitlab.com/user/workspace/settings/#shared_namespace' + image_url: https://img.youtube.com/vi/CXakdRuoGgU/hqdefault.jpg + published_at: 2025-05-15 + release: 18.0 +- name: "Event data collection" + description: | + In GitLab 18.0, we are enabling event-level product usage data collection from GitLab Self-Managed and GitLab Dedicated instances. Unlike aggregated data, event-level data provides GitLab with deeper insights into usage, allowing us to improve user experience on the platform and increase feature adoption. For detailed instructions on how to adjust data sharing settings, please refer to our documentation. + stage: monitor + self-managed: true + gitlab-com: false + available_in: [Free, Premium, Ultimate] + documentation_link: 'https://docs.gitlab.com/administration/settings/event_data/' + published_at: 2025-05-15 + release: 18.0 diff --git a/db/docs/batched_background_migrations/fix_incomplete_external_audit_destinations.yml b/db/docs/batched_background_migrations/fix_incomplete_external_audit_destinations.yml new file mode 100644 index 00000000000..be77ce14ae0 --- /dev/null +++ b/db/docs/batched_background_migrations/fix_incomplete_external_audit_destinations.yml @@ -0,0 +1,8 @@ +--- +migration_job_name: FixIncompleteExternalAuditDestinations +description: Fix incomplete migrations of dependency tables for audit events +feature_category: audit_events +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189699 +milestone: '18.1' +queued_migration_version: 20250429171748 +finalized_by: # version of the migration that finalized this BBM diff --git a/db/docs/batched_background_migrations/fix_incomplete_instance_external_audit_destinations.yml b/db/docs/batched_background_migrations/fix_incomplete_instance_external_audit_destinations.yml new file mode 100644 index 00000000000..0cd0a1ded8e --- /dev/null +++ b/db/docs/batched_background_migrations/fix_incomplete_instance_external_audit_destinations.yml @@ -0,0 +1,8 @@ +--- +migration_job_name: FixIncompleteInstanceExternalAuditDestinations +description: Fix incomplete migrations of dependency tables for audit events +feature_category: audit_events +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189699 +milestone: '18.1' +queued_migration_version: 20250429171801 +finalized_by: # version of the migration that finalized this BBM diff --git a/db/post_migrate/20250310144538_queue_backfill_external_group_audit_event_destinations.rb b/db/post_migrate/20250310144538_queue_backfill_external_group_audit_event_destinations.rb index 80af6d0c83e..7246b505044 100644 --- a/db/post_migrate/20250310144538_queue_backfill_external_group_audit_event_destinations.rb +++ b/db/post_migrate/20250310144538_queue_backfill_external_group_audit_event_destinations.rb @@ -12,7 +12,7 @@ class QueueBackfillExternalGroupAuditEventDestinations < Gitlab::Database::Migra def up # no-op because there was a bug in the original migration (double JSON encoding), - # which has been fixed by QueueBackfillExternalGroupAuditEventDestinationsFixed + # which has been fixed by QueueFixIncompleteInstanceExternalAuditDestinations end def down; end diff --git a/db/post_migrate/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed.rb b/db/post_migrate/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed.rb index d0f794d7b49..c82c5467b69 100644 --- a/db/post_migrate/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed.rb +++ b/db/post_migrate/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed.rb @@ -11,18 +11,9 @@ class QueueBackfillExternalGroupAuditEventDestinationsFixed < Gitlab::Database:: SUB_BATCH_SIZE = 10 def up - delete_batched_background_migration(ORIGINAL_MIGRATION, :audit_events_external_audit_event_destinations, :id, []) - - queue_batched_background_migration( - MIGRATION, - :audit_events_external_audit_event_destinations, - :id, - batch_size: BATCH_SIZE, - sub_batch_size: SUB_BATCH_SIZE - ) + # no-op because there was a bug in the migration + # replaced by QueueFixIncompleteInstanceExternalAuditDestinations end - def down - delete_batched_background_migration(MIGRATION, :audit_events_external_audit_event_destinations, :id, []) - end + def down; end end diff --git a/db/post_migrate/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed.rb b/db/post_migrate/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed.rb index d67dee46798..1504740d9f7 100644 --- a/db/post_migrate/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed.rb +++ b/db/post_migrate/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed.rb @@ -11,19 +11,9 @@ class QueueBackfillExternalInstanceAuditEventDestinationsFixed < Gitlab::Databas SUB_BATCH_SIZE = 10 def up - delete_batched_background_migration(ORIGINAL_MIGRATION, :audit_events_instance_external_audit_event_destinations, - :id, []) - - queue_batched_background_migration( - MIGRATION, - :audit_events_instance_external_audit_event_destinations, - :id, - batch_size: BATCH_SIZE, - sub_batch_size: SUB_BATCH_SIZE - ) + # no-op because there was a bug in the migration + # replaced by QueueFixIncompleteExternalAuditDestinations end - def down - delete_batched_background_migration(MIGRATION, :audit_events_instance_external_audit_event_destinations, :id, []) - end + def down; end end diff --git a/db/post_migrate/20250429171748_queue_fix_incomplete_external_audit_destinations.rb b/db/post_migrate/20250429171748_queue_fix_incomplete_external_audit_destinations.rb new file mode 100644 index 00000000000..c273692bf24 --- /dev/null +++ b/db/post_migrate/20250429171748_queue_fix_incomplete_external_audit_destinations.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class QueueFixIncompleteExternalAuditDestinations < Gitlab::Database::Migration[2.3] + milestone '18.1' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "FixIncompleteExternalAuditDestinations" + BATCH_SIZE = 100 + SUB_BATCH_SIZE = 10 + + def up + queue_batched_background_migration( + MIGRATION, + :audit_events_external_audit_event_destinations, + :id, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :audit_events_external_audit_event_destinations, :id, []) + end +end diff --git a/db/post_migrate/20250429171801_queue_fix_incomplete_instance_external_audit_destinations.rb b/db/post_migrate/20250429171801_queue_fix_incomplete_instance_external_audit_destinations.rb new file mode 100644 index 00000000000..680d5d7c619 --- /dev/null +++ b/db/post_migrate/20250429171801_queue_fix_incomplete_instance_external_audit_destinations.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class QueueFixIncompleteInstanceExternalAuditDestinations < Gitlab::Database::Migration[2.3] + milestone '18.1' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "FixIncompleteInstanceExternalAuditDestinations" + BATCH_SIZE = 100 + SUB_BATCH_SIZE = 10 + + def up + queue_batched_background_migration( + MIGRATION, + :audit_events_instance_external_audit_event_destinations, + :id, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :audit_events_instance_external_audit_event_destinations, :id, []) + end +end diff --git a/db/schema_migrations/20250429171748 b/db/schema_migrations/20250429171748 new file mode 100644 index 00000000000..3dc3ef01567 --- /dev/null +++ b/db/schema_migrations/20250429171748 @@ -0,0 +1 @@ +d30e225a8dfbe92bd8dcb7b3fb1483bf0b1689633aac799f25bef9492362e928 \ No newline at end of file diff --git a/db/schema_migrations/20250429171801 b/db/schema_migrations/20250429171801 new file mode 100644 index 00000000000..70caae215a5 --- /dev/null +++ b/db/schema_migrations/20250429171801 @@ -0,0 +1 @@ +f65a4a840431a711e8f8ad0e1e2628942fdbfd09318bba87147c03a8c0866277 \ No newline at end of file diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index ff6a8ea894e..f8011d88e74 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -24079,6 +24079,7 @@ Represents a ComplianceRequirementsControl associated with a ComplianceRequireme | ---- | ---- | ----------- | | `controlType` | [`String!`](#string) | Type of the compliance control. | | `expression` | [`String`](#string) | Expression of the compliance control. | +| `externalControlName` | [`String`](#string) | Name of the external control. | | `externalUrl` | [`String`](#string) | URL of the external control. | | `id` | [`ID!`](#id) | Compliance requirements control ID. | | `name` | [`String!`](#string) | Name of the compliance control. | @@ -49539,6 +49540,7 @@ Attributes for defining a CI/CD variable. | ---- | ---- | ----------- | | `controlType` | [`String`](#string) | Type of the compliance control. | | `expression` | [`String`](#string) | Expression of the compliance control. | +| `externalControlName` | [`String`](#string) | Name of the external control. | | `externalUrl` | [`String`](#string) | URL of the external control. | | `name` | [`String!`](#string) | New name for the compliance requirement control. | | `secretToken` | [`String`](#string) | Secret token for an external control. | diff --git a/doc/user/compliance/compliance_center/compliance_standards_adherence_dashboard.md b/doc/user/compliance/compliance_center/compliance_standards_adherence_dashboard.md index 48ccd233991..b842d30358a 100644 --- a/doc/user/compliance/compliance_center/compliance_standards_adherence_dashboard.md +++ b/doc/user/compliance/compliance_center/compliance_standards_adherence_dashboard.md @@ -2,9 +2,11 @@ stage: Software Supply Chain Security group: Compliance info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments -title: Compliance standards adherence dashboard +title: Compliance standards adherence dashboard (deprecated) --- + + {{< details >}} - Tier: Ultimate @@ -12,6 +14,13 @@ title: Compliance standards adherence dashboard {{< /details >}} +{{< alert type="warning" >}} + +This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/470834) in GitLab 17.11 +and is planned for removal in 18.6. Use the [compliance status report](compliance_status_report.md) instead. + +{{< /alert >}} + {{< history >}} - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125875) GraphQL APIs in GitLab 16.2 [with a flag](../../../administration/feature_flags.md) named `compliance_adherence_report`. Disabled by default. @@ -150,3 +159,5 @@ To export the compliance standards adherence report for projects in a group: 1. Select **Export standards adherence report**. A report is compiled and delivered to your email inbox as an attachment. + + diff --git a/lib/gitlab/background_migration/fix_incomplete_external_audit_destinations.rb b/lib/gitlab/background_migration/fix_incomplete_external_audit_destinations.rb new file mode 100644 index 00000000000..a952bb2ee4f --- /dev/null +++ b/lib/gitlab/background_migration/fix_incomplete_external_audit_destinations.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This batched background migration is EE-only + class FixIncompleteExternalAuditDestinations < BatchedMigrationJob + feature_category :audit_events + + def perform; end + end + end +end + +Gitlab::BackgroundMigration::FixIncompleteExternalAuditDestinations.prepend_mod diff --git a/lib/gitlab/background_migration/fix_incomplete_instance_external_audit_destinations.rb b/lib/gitlab/background_migration/fix_incomplete_instance_external_audit_destinations.rb new file mode 100644 index 00000000000..bd4c288343b --- /dev/null +++ b/lib/gitlab/background_migration/fix_incomplete_instance_external_audit_destinations.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This batched background migration is EE-only + class FixIncompleteInstanceExternalAuditDestinations < BatchedMigrationJob + feature_category :audit_events + + def perform; end + end + end +end + +Gitlab::BackgroundMigration::FixIncompleteInstanceExternalAuditDestinations.prepend_mod diff --git a/lib/gitlab/fp/result.rb b/lib/gitlab/fp/result.rb index 292498bbdce..4bb22a8c484 100644 --- a/lib/gitlab/fp/result.rb +++ b/lib/gitlab/fp/result.rb @@ -51,6 +51,7 @@ module Gitlab @ok = err_value.nil? @value = ok? ? ok_value : err_value end + private :initialize # "#unwrap" corresponds to "unwrap" in Rust. @@ -110,7 +111,7 @@ module Gitlab # @return [Result] # @raise [TypeError] def and_then(lambda_or_singleton_method) - validate_lambda_or_singleton_method(lambda_or_singleton_method) + validate_lambda_or_singleton_method(callee: lambda_or_singleton_method, invoking_method: __method__) # Return/passthough the Result itself if it is an err return self if err? @@ -119,7 +120,7 @@ module Gitlab result = lambda_or_singleton_method.call(value) unless result.is_a?(Result) - err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns a 'Result' " \ + err_msg = "Result##{__method__} expects a lambda or singleton method object which returns a 'Result' " \ "type, but instead received '#{lambda_or_singleton_method.inspect}' which returned '#{result.class}'. " \ "Check that the previous method calls in the '#and_then' chain are correct." raise(TypeError, err_msg) @@ -145,7 +146,7 @@ module Gitlab # @return [Result] # @raise [TypeError] def map(lambda_or_singleton_method) - validate_lambda_or_singleton_method(lambda_or_singleton_method) + validate_lambda_or_singleton_method(callee: lambda_or_singleton_method, invoking_method: __method__) # Return/passthrough the Result itself if it is an err return self if err? @@ -154,7 +155,7 @@ module Gitlab mapped_value = lambda_or_singleton_method.call(value) if mapped_value.is_a?(Result) - err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns an unwrapped " \ + err_msg = "Result##{__method__} expects a lambda or singleton method object which returns an unwrapped " \ "value, not a 'Result', but instead received '#{lambda_or_singleton_method.inspect}' which returned " \ "a 'Result'." raise(TypeError, err_msg) @@ -164,6 +165,99 @@ module Gitlab Result.ok(mapped_value) end + # `map_err` is the inverse of `map`. It behaves identically, but it only processes `err` values + # instead of `ok` values. + # + # "#map_err" corresponds to "map_err" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err + # + # @param [Proc, Method] lambda_or_singleton_method + # @return [Result] + # @raise [TypeError] + def map_err(lambda_or_singleton_method) + validate_lambda_or_singleton_method(callee: lambda_or_singleton_method, invoking_method: __method__) + + # Return/passthrough the Result itself if it is an ok + return self if ok? + + # If the Result is err, call the lambda or singleton method with the contained value + mapped_value = lambda_or_singleton_method.call(value) + + if mapped_value.is_a?(Result) + err_msg = "Result##{__method__} expects a lambda or singleton method object which returns an unwrapped " \ + "value, not a 'Result', but instead received '#{lambda_or_singleton_method.inspect}' which returned " \ + "a 'Result'." + raise(TypeError, err_msg) + end + + # wrap the returned mapped_value in an "err" Result. + Result.err(mapped_value) + end + + # `inspect_ok` is similar to `map`, becuase it receives the wrapped `ok` value, but it does not allow modification + # of the value like `map`. The original result is always returned from `inspect_ok`. + # + # The passed lambda or singleton method must return, `nil`, to enforce the fact that the return value is ignored, + # and the original Result is always returned. This corresponds to the `void` type in YARD/RBS type annotations, + # and the `unit` type in Rust (https://doc.rust-lang.org/std/primitive.unit.html). + # + # If the passed method does not return `nil`, an error will be raised. + # + # "#inspect_ok" corresponds to "inspect" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect + # + # But note that we could not call it `inspect` here, because that would conflict with the + # Kernel#inspect method in Ruby. + # + # @param [Proc, Method] lambda_or_singleton_method + # @return [Result] + # @raise [TypeError] + def inspect_ok(lambda_or_singleton_method) + validate_lambda_or_singleton_method(callee: lambda_or_singleton_method, invoking_method: __method__) + + # Return/passthrough the Result itself if it is an err + return self if err? + + # If the Result is ok, call the lambda or singleton method with the contained value + call_and_enforce_value_is_not_mutated( + callee: lambda_or_singleton_method, + value: value, + invoking_method: __method__ + ) + + # Return/passthrough the original Result + self + end + + # `inspect_err` is the inverse of `inspect_ok`. It behaves identically, but it only processes `err` values + # instead of `ok` values. + # + # The passed lambda or singleton method must return, `nil`, to enforce the fact that the return value is ignored, + # and the original Result is always returned. This corresponds to the `void` type in YARD/RBS type annotations, + # and the `unit` type in Rust (https://doc.rust-lang.org/std/primitive.unit.html). + # + # If the passed method does not return `nil`, an error will be raised. + # + # "#inspect_err" corresponds to "inspect_err" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err + # + # @param [Proc, Method] lambda_or_singleton_method + # @return [Result] + # @raise [TypeError] + def inspect_err(lambda_or_singleton_method) + validate_lambda_or_singleton_method(callee: lambda_or_singleton_method, invoking_method: __method__) + + # Return/passthrough the Result itself if it is an ok + return self if ok? + + # If the Result is err, call the lambda or singleton method with the contained value + call_and_enforce_value_is_not_mutated( + callee: lambda_or_singleton_method, + value: value, + invoking_method: __method__ + ) + + # Return/passthrough the original Result + self + end + # `to_h` supports destructuring of a result object, for example: `result => { ok: }; puts ok` # # @return [Hash] @@ -205,29 +299,80 @@ module Gitlab @value end - # @param [Proc, Method] lambda_or_singleton_method + # @param [Proc, Method] callee + # @param [Symbol] invoking_method # @return [void] # @raise [TypeError] - def validate_lambda_or_singleton_method(lambda_or_singleton_method) - is_lambda = lambda_or_singleton_method.is_a?(Proc) && lambda_or_singleton_method.lambda? + def validate_lambda_or_singleton_method(callee:, invoking_method:) + is_lambda = callee.is_a?(Proc) && callee.lambda? is_singleton_method = - lambda_or_singleton_method.is_a?(Method) && lambda_or_singleton_method.owner.singleton_class? + callee.is_a?(Method) && callee.owner.singleton_class? unless is_lambda || is_singleton_method - err_msg = "'Result##{__method__}' expects a lambda or singleton method object, " \ - "but instead received '#{lambda_or_singleton_method.inspect}'." + err_msg = "Result##{invoking_method} expects a lambda or singleton method object, " \ + "but instead received '#{callee.inspect}'." raise(TypeError, err_msg) end - arity = lambda_or_singleton_method.arity + arity = callee.arity return if arity == 1 - return if arity == -1 && lambda_or_singleton_method.source_location[0].include?('rspec') + return if arity == -1 && callee.source_location[0].include?('rspec') - err_msg = "'Result##{__method__}' expects a lambda or singleton method object with a single argument " \ - "(arity of 1), but instead received '#{lambda_or_singleton_method.inspect}' with an arity of #{arity}." + err_msg = "Result##{invoking_method} expects a lambda or singleton method object with a single argument " \ + "(arity of 1), but instead received '#{callee.inspect}' with an arity of #{arity}." raise(ArgumentError, err_msg) end + + # @param [Proc, Method] callee + # @param [Object] value + # @param [Symbol] invoking_method + # @return [void] + # @raise [RuntimeError] + def call_and_enforce_value_is_not_mutated(callee:, value:, invoking_method:) + value_before = value.clone + + begin + marshalled_value_before = Marshal.dump(value) + rescue StandardError + # Marshal.dump will fail if there are singletons in the object + marshalled_value_before = nil + end + + return_value_from_call = callee.call(value) + + validate_return_value_is_void(return_value: return_value_from_call, invoking_method: invoking_method) + + begin + marshalled_value_after = Marshal.dump(value) + rescue StandardError + # Marshal.dump will fail if there are singletons in the object + marshalled_value_after = nil + end + + value_was_mutated = + # First do an equality check, but this might return a false positive for some deeply nested objects + # or objects which don't implement equality properly, so also do the marshalled value equality check + value_before != value || marshalled_value_before != marshalled_value_after + + return unless value_was_mutated + + raise "ERROR: #{callee} must not modify the passed value argument, " \ + "because it was invoked via Result##{invoking_method}" + end + + # @param [Proc, Method] return_value + # @param [Symbol] invoking_method + # @return [void] + # @raise [TypeError] + def validate_return_value_is_void(return_value:, invoking_method:) + return if return_value.nil? + + err_msg = "The method passed to Result##{invoking_method} must always return 'nil' (void). This enforces " \ + "that the return value is never used or modified. The existing 'Result' object is always passed along the " \ + "chain unchanged. The return value received was '#{return_value.inspect}' instead of 'nil'." + raise(TypeError, err_msg) + end end end end diff --git a/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js b/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js index 5805b4c5634..67de256396b 100644 --- a/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js @@ -127,6 +127,7 @@ describe('Code Dropdown component', () => { expect(item.props('item').text).toBe(name); expect(item.props('item').href).toContain(href); + expect(item.props('item').extraAttrs.isUnsafeLink).toBe(true); }); it('closes the dropdown on click', () => { diff --git a/spec/lib/gitlab/fp/result_spec.rb b/spec/lib/gitlab/fp/result_spec.rb index 2bfd5634786..505e1dc6cc5 100644 --- a/spec/lib/gitlab/fp/result_spec.rb +++ b/spec/lib/gitlab/fp/result_spec.rb @@ -6,9 +6,14 @@ require 'fast_spec_helper' # This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type. # These examples can be executed as-is in a Rails console to see the results. # -# To support this, we have intentionally used some `rubocop:disable` comments to allow for more -# explicit and readable examples. +# To support this, we have intentionally used some `rubocop:disable` and RubyMine `noinspection` comments +# to allow for more explicit and readable examples. +# +# There is also not much attempt to DRY up the examples. There is some duplication, but this is intentional to +# support easily understandable and readable examples. +# # rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration -- intentionally disabled per comment above +# noinspection MissingYardReturnTag, MissingYardParamTag - intentionally disabled per comment above RSpec.describe Gitlab::Fp::Result, feature_category: :shared do describe 'usage of Gitlab::Fp::Result.ok and Gitlab::Fp::Result.err' do context 'when checked with .ok? and .err?' do @@ -159,7 +164,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do describe 'enforcement of argument type' do it 'raises TypeError if passed anything other than a lambda or singleton method object' do ex = TypeError - msg = /expects a lambda or singleton method object/ + msg = /Result#and_then expects a lambda or singleton method object/ # noinspection RubyMismatchedArgumentType -- intentionally passing invalid types expect { Gitlab::Fp::Result.ok(1).and_then('string') }.to raise_error(ex, msg) expect { Gitlab::Fp::Result.ok(1).and_then(proc { Gitlab::Fp::Result.ok(1) }) }.to raise_error(ex, msg) @@ -172,7 +177,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do expect do Gitlab::Fp::Result.ok(1).and_then(->(a, b) { Gitlab::Fp::Result.ok(a + b) }) - end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/) + end.to raise_error(ArgumentError, /Result#and_then expects .* with a single argument \(arity of 1\)/) end end @@ -180,7 +185,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do it 'raises ArgumentError if passed lambda or singleton method object which returns non-Result type' do expect do Gitlab::Fp::Result.ok(1).and_then(->(a) { a + 1 }) - end.to raise_error(TypeError, /expects .* which returns a 'Result' type/) + end.to raise_error(TypeError, /Result#and_then expects .* which returns a 'Result' type/) end end end @@ -252,7 +257,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do describe 'enforcement of argument type' do it 'raises TypeError if passed anything other than a lambda or singleton method object' do ex = TypeError - msg = /expects a lambda or singleton method object/ + msg = /Result#map expects a lambda or singleton method object/ # noinspection RubyMismatchedArgumentType -- intentionally passing invalid types expect { Gitlab::Fp::Result.ok(1).map('string') }.to raise_error(ex, msg) expect { Gitlab::Fp::Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg) @@ -265,7 +270,7 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do expect do Gitlab::Fp::Result.ok(1).map(->(a, b) { a + b }) - end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/) + end.to raise_error(ArgumentError, /Result#map expects .* with a single argument \(arity of 1\)/) end end @@ -273,7 +278,318 @@ RSpec.describe Gitlab::Fp::Result, feature_category: :shared do it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do expect do Gitlab::Fp::Result.ok(1).map(->(a) { Gitlab::Fp::Result.ok(a + 1) }) - end.to raise_error(TypeError, /expects .* which returns an unwrapped value, not a 'Result'/) + end.to raise_error(TypeError, /Result#map expects .* which returns an unwrapped value, not a 'Result'/) + end + end + end + end + + describe 'usage of #map_err' do + context 'when passed a proc' do + it 'ignores ok values in successful chain' do + initial_result = Gitlab::Fp::Result.ok(1) + final_result = + initial_result + .map_err(->(value) { value + 1 }) + + expect(final_result.ok?).to be(true) + expect(final_result.unwrap).to eq(1) + end + + it 'returns first err value in failed chain' do + initial_result = Gitlab::Fp::Result.ok(1) + final_result = + initial_result + .and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") }) + .map_err(->(value) { "#{value}, with map_err" }) + + expect(final_result.err?).to be(true) + expect(final_result.unwrap_err).to eq('invalid: 1, with map_err') + end + end + + context 'when passed a module or class (singleton) method object' do + module MyModuleNotUsingResult + def self.double(value) + value * 2 + end + + class MyClassNotUsingResult + def self.triple(value) + value * 3 + end + end + end + + it 'processes the err value in failed chain' do + initial_result = Gitlab::Fp::Result.err(1) + final_result = + initial_result + .map_err(::MyModuleNotUsingResult.method(:double)) + .map_err(::MyModuleNotUsingResult::MyClassNotUsingResult.method(:triple)) + + expect(final_result.err?).to be(true) + expect(final_result.unwrap_err).to eq(6) + end + end + + describe 'type checking validation' do + describe 'enforcement of argument type' do + it 'raises TypeError if passed anything other than a lambda or singleton method object' do + ex = TypeError + msg = /Result#map_err expects a lambda or singleton method object/ + # noinspection RubyMismatchedArgumentType -- intentionally passing invalid types + expect { Gitlab::Fp::Result.ok(1).map_err('string') }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).map_err(proc { 1 }) }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).map_err(1.method(:to_s)) }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).map_err(Integer.method(:to_s)) }.to raise_error(ex, msg) + end + end + + describe 'enforcement of argument arity' do + it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do + expect do + Gitlab::Fp::Result.ok(1).map_err(->(a, b) { a + b }) + end.to raise_error(ArgumentError, /Result#map_err expects .* with a single argument \(arity of 1\)/) + end + end + + describe 'enforcement that passed lambda or method does not return a Result type' do + it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do + expect do + Gitlab::Fp::Result.err(1).map_err(->(a) { Gitlab::Fp::Result.ok(a + 1) }) + end.to raise_error(TypeError, /Result#map_err expects .* which returns an unwrapped value, not a 'Result'/) + end + end + end + end + + describe 'usage of #inspect_ok' do + let(:logger) { instance_double(Logger, :info) } + + context 'when passed a proc' do + it 'returns last ok value in successful chain and performs side effect' do + expect(logger).to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .inspect_ok(->(context) { context[:logger].info }) + + expect(final_result.ok?).to be(true) + expect(final_result.unwrap).to eq({ logger: logger }) + end + + it 'returns first err value in failed chain and does not perform side effect' do + expect(logger).not_to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") }) + .inspect_ok(->(context) { context[:logger].info }) + + expect(final_result.err?).to be(true) + expect(final_result.unwrap_err).to match(/invalid:.*logger.*:info/) + end + + it 'cannot modify the Result passed along the chain', :unlimited_max_formatted_output_length do + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + expect do + initial_result.inspect_ok(->(context) { context[:logger] = nil }) + end.to raise_error( + RuntimeError, /Proc:.*must not modify the passed value.*because it was invoked via Result#inspect_ok/ + ) + end + end + + context 'when passed a module or class (singleton) method object' do + module MyModuleNotUsingResult + def self.observe(context) + context[:logger].info + end + + def self.modify!(context) + context[:logger] = "MODIFIED VALUE" + nil + end + end + + it 'returns last ok value in successful chain and performs side effect' do + expect(logger).to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .inspect_ok(::MyModuleNotUsingResult.method(:observe)) + + expect(final_result.ok?).to be(true) + expect(final_result.unwrap).to eq({ logger: logger }) + end + + it 'returns first err value in failed chain and does not perform side effect' do + expect(logger).not_to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .and_then(->(value) { Gitlab::Fp::Result.err("invalid: #{value}") }) + .inspect_ok(::MyModuleNotUsingResult.method(:observe)) + + expect(final_result.err?).to be(true) + expect(final_result.unwrap_err).to match(/invalid:.*logger.*:info/) + end + + it 'cannot modify the Result passed along the chain', :unlimited_max_formatted_output_length do + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + expect do + initial_result.inspect_ok(::MyModuleNotUsingResult.method(:modify!)) + end.to raise_error( + RuntimeError, + /Method: MyModuleNotUsingResult.modify!\(context\).*not modify the.*value.*invoked via Result#inspect_ok/ + ) + end + end + + describe 'type checking validation' do + describe 'enforcement of argument type' do + it 'raises TypeError if passed anything other than a lambda or singleton method object', + :unlimited_max_formatted_output_length do + ex = TypeError + msg = /Result#inspect_ok expects a lambda or singleton method object/ + # noinspection RubyMismatchedArgumentType -- intentionally passing invalid types + expect { Gitlab::Fp::Result.ok(1).inspect_ok('string') }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).inspect_ok(proc { 1 }) }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).inspect_ok(1.method(:to_s)) }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).inspect_ok(Integer.method(:to_s)) }.to raise_error(ex, msg) + end + end + + describe 'enforcement of argument arity' do + it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do + expect do + Gitlab::Fp::Result.ok(1).inspect_ok(->(a, b) { a + b }) + end.to raise_error(ArgumentError, /Result#inspect_ok expects .* with a single argument \(arity of 1\)/) + end + end + + describe 'enforcement that passed lambda or method returns nil (void)' do + it 'raises TypeError if passed lambda or singleton method object which does not return nil' do + expect do + Gitlab::Fp::Result.ok(1).inspect_ok(->(_) { "not nil" }) + end.to raise_error(TypeError, /Result#inspect_ok.*must always return 'nil'/) + end + end + end + end + + describe 'usage of #inspect_err' do + let(:logger) { instance_double(Logger, :info) } + + context 'when passed a proc' do + it 'returns last ok value in successful chain and does not performs side effect' do + expect(logger).not_to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .inspect_err(->(context) { context[:logger].info }) + + expect(final_result.ok?).to be(true) + expect(final_result.unwrap).to eq({ logger: logger }) + end + + it 'returns first err value in failed chain and performs side effect' do + expect(logger).to receive(:info) + + initial_result = Gitlab::Fp::Result.err({ logger: logger }) + final_result = + initial_result + .inspect_err(->(context) { context[:logger].info }) + + expect(final_result.err?).to be(true) + expect(final_result.unwrap_err).to eq({ logger: logger }) + end + + it 'cannot modify the Result passed along the chain', :unlimited_max_formatted_output_length do + initial_result = Gitlab::Fp::Result.err({ logger: logger }) + expect do + initial_result.inspect_err(->(context) { context[:logger] = nil }) + end.to raise_error( + RuntimeError, /Proc:.*must not modify the passed value.*because it was invoked via Result.inspect_err/ + ) + end + end + + context 'when passed a module or class (singleton) method object' do + module MyModuleNotUsingResult + def self.observe(context) + context[:logger].info + end + end + + it 'returns last ok value in successful chain and does not perform side effect' do + expect(logger).not_to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .inspect_err(::MyModuleNotUsingResult.method(:observe)) + + expect(final_result.ok?).to be(true) + expect(final_result.unwrap).to eq({ logger: logger }) + end + + it 'returns first err value in failed chain and performs side effect' do + expect(logger).to receive(:info) + + initial_result = Gitlab::Fp::Result.ok({ logger: logger }) + final_result = + initial_result + .and_then(->(value) { Gitlab::Fp::Result.err(value) }) + .inspect_err(::MyModuleNotUsingResult.method(:observe)) + + expect(final_result.err?).to be(true) + expect(final_result.unwrap_err).to eq({ logger: logger }) + end + + it 'cannot modify the Result passed along the chain', :unlimited_max_formatted_output_length do + initial_result = Gitlab::Fp::Result.err({ logger: logger }) + expect do + initial_result.inspect_err(::MyModuleNotUsingResult.method(:modify!)) + end.to raise_error( + RuntimeError, + /Method: MyModuleNotUsingResult.modify!\(context\).*not modify the.*value.*invoked via Result#inspect_err/ + ) + end + end + + describe 'type checking validation' do + describe 'enforcement of argument type' do + it 'raises TypeError if passed anything other than a lambda or singleton method object' do + ex = TypeError + msg = /Result#inspect_err expects a lambda or singleton method object/ + # noinspection RubyMismatchedArgumentType -- intentionally passing invalid types + expect { Gitlab::Fp::Result.ok(1).inspect_err('str') }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).inspect_err(proc { 1 }) }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).inspect_err(1.method(:to_s)) }.to raise_error(ex, msg) + expect { Gitlab::Fp::Result.ok(1).inspect_err(Integer.method(:to_s)) }.to raise_error(ex, msg) + end + end + + describe 'enforcement of argument arity' do + it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do + expect do + Gitlab::Fp::Result.err(1).inspect_err(->(a, b) { a + b }) + end.to raise_error(ArgumentError, /Result#inspect_err expects .* with a single argument \(arity of 1\)/) + end + end + + describe 'enforcement that passed lambda or method returns nil (void)' do + it 'raises TypeError if passed lambda or singleton method object which does not return nil' do + expect do + Gitlab::Fp::Result.err(1).inspect_err(->(_) { "not nil" }) + end.to raise_error(TypeError, /Result#inspect_err.*must always return 'nil'/) end end end diff --git a/spec/migrations/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed_spec.rb b/spec/migrations/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed_spec.rb index 930ac48daf0..e4eee5024f0 100644 --- a/spec/migrations/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed_spec.rb +++ b/spec/migrations/20250403155636_queue_backfill_external_group_audit_event_destinations_fixed_spec.rb @@ -8,30 +8,10 @@ RSpec.describe QueueBackfillExternalGroupAuditEventDestinationsFixed, feature_category: :audit_events do let(:batched_migration) { described_class::MIGRATION } - it 'schedules a new batched migration' do - reversible_migration do |migration| - migration.before -> { - expect(batched_migration).not_to have_scheduled_batched_migration - } + it 'is a no-op migration' do + # Simply verify that up and down do nothing + expect { migrate! }.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } - migration.after -> { - expect(batched_migration).to have_scheduled_batched_migration( - table_name: :audit_events_external_audit_event_destinations, - column_name: :id, - batch_size: described_class::BATCH_SIZE, - sub_batch_size: described_class::SUB_BATCH_SIZE, - gitlab_schema: :gitlab_main - ) - } - end - end - - it 'removes scheduled migration when rolling back' do - disable_migrations_output do - migrate! - schema_migrate_down! - end - - expect(batched_migration).not_to have_scheduled_batched_migration + expect { schema_migrate_down! }.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } end end diff --git a/spec/migrations/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed_spec.rb b/spec/migrations/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed_spec.rb index b9eb0f8016c..61d61b9f110 100644 --- a/spec/migrations/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed_spec.rb +++ b/spec/migrations/20250403155703_queue_backfill_external_instance_audit_event_destinations_fixed_spec.rb @@ -8,30 +8,10 @@ RSpec.describe QueueBackfillExternalInstanceAuditEventDestinationsFixed, feature_category: :audit_events do let(:batched_migration) { described_class::MIGRATION } - it 'schedules a new batched migration' do - reversible_migration do |migration| - migration.before -> { - expect(batched_migration).not_to have_scheduled_batched_migration - } + it 'is a no-op migration' do + # Simply verify that up and down do nothing + expect { migrate! }.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } - migration.after -> { - expect(batched_migration).to have_scheduled_batched_migration( - table_name: :audit_events_instance_external_audit_event_destinations, - column_name: :id, - batch_size: described_class::BATCH_SIZE, - sub_batch_size: described_class::SUB_BATCH_SIZE, - gitlab_schema: :gitlab_main - ) - } - end - end - - it 'removes scheduled migration when rolling back' do - disable_migrations_output do - migrate! - schema_migrate_down! - end - - expect(batched_migration).not_to have_scheduled_batched_migration + expect { schema_migrate_down! }.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } end end diff --git a/spec/migrations/20250429171748_queue_fix_incomplete_external_audit_destinations_spec.rb b/spec/migrations/20250429171748_queue_fix_incomplete_external_audit_destinations_spec.rb new file mode 100644 index 00000000000..c43e401cfd6 --- /dev/null +++ b/spec/migrations/20250429171748_queue_fix_incomplete_external_audit_destinations_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueFixIncompleteExternalAuditDestinations, + migration: :gitlab_main, + feature_category: :audit_events do + let(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :audit_events_external_audit_event_destinations, + column_name: :id, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE, + gitlab_schema: :gitlab_main + ) + } + end + end + + it 'removes scheduled migration when rolling back' do + disable_migrations_output do + migrate! + schema_migrate_down! + end + + expect(batched_migration).not_to have_scheduled_batched_migration + end +end diff --git a/spec/migrations/20250429171801_queue_fix_incomplete_instance_external_audit_destinations_spec.rb b/spec/migrations/20250429171801_queue_fix_incomplete_instance_external_audit_destinations_spec.rb new file mode 100644 index 00000000000..08d8d05bd7c --- /dev/null +++ b/spec/migrations/20250429171801_queue_fix_incomplete_instance_external_audit_destinations_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueFixIncompleteInstanceExternalAuditDestinations, + migration: :gitlab_main, + feature_category: :audit_events do + let(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :audit_events_instance_external_audit_event_destinations, + column_name: :id, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE, + gitlab_schema: :gitlab_main + ) + } + end + end + + it 'removes scheduled migration when rolling back' do + disable_migrations_output do + migrate! + schema_migrate_down! + end + + expect(batched_migration).not_to have_scheduled_batched_migration + end +end diff --git a/spec/support/matchers/invoke_rop_steps.rb b/spec/support/matchers/invoke_rop_steps.rb index 9c4724c1ddc..d8b9830dd47 100644 --- a/spec/support/matchers/invoke_rop_steps.rb +++ b/spec/support/matchers/invoke_rop_steps.rb @@ -64,9 +64,9 @@ module InvokeRopSteps "but was a #{step_action.class}" end - unless [:map, :and_then].freeze.include?(step_action) - raise "'invoke_rop_steps' argument array entry second element ':#{step_action}' must be either " \ - ":map or :and_then, but was :#{step_action}" + unless [:and_then, :map, :map_err, :inspect_ok, :inspect_err].freeze.include?(step_action) + raise "'invoke_rop_steps' argument array entry second element ':#{step_action}' must be one of " \ + ":and_then, :map, :map_err, :inspect_ok, or :inspect_err, but was :#{step_action}" end end end @@ -132,8 +132,10 @@ module InvokeRopSteps expected_rop_step[:returned_object] = ok_results_for_steps[step_class] elsif step_action == :and_then expected_rop_step[:returned_object] = Gitlab::Fp::Result.ok(context_passed_along_steps) - elsif step_action == :map + elsif [:map, :map_err].freeze.include?(step_action) expected_rop_step[:returned_object] = context_passed_along_steps + elsif [:inspect_ok, :inspect_err].freeze.include?(step_action) + expected_rop_step[:returned_object] = nil else raise "Unexpected internal error when building expected ROP steps: step_action '#{step_action}' is invalid" end @@ -149,7 +151,7 @@ module InvokeRopSteps step => { step_class: Class => step_class, step_class_method: Symbol => step_class_method, - returned_object: Gitlab::Fp::Result | Hash => returned_object + returned_object: Gitlab::Fp::Result | Hash | nil => returned_object } set_up_step_class_expectation(