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(