diff --git a/Gemfile b/Gemfile index 6f2ae29d566..45661ce48a9 100644 --- a/Gemfile +++ b/Gemfile @@ -253,7 +253,7 @@ gem 'gitlab-active-context', path: 'gems/gitlab-active-context', require: 'activ # Markdown and HTML processing gem 'html-pipeline', '~> 2.14.3', feature_category: :markdown gem 'deckar01-task_list', '2.3.4', feature_category: :markdown -gem 'gitlab-markup', '~> 1.10.0', require: 'github/markup', feature_category: :markdown +gem 'gitlab-markup', '~> 2.0.0', require: 'github/markup', feature_category: :markdown gem 'commonmarker', '~> 0.23.10', feature_category: :markdown gem 'kramdown', '~> 2.5.0', feature_category: :markdown gem 'RedCloth', '~> 4.3.3', feature_category: :markdown diff --git a/Gemfile.checksum b/Gemfile.checksum index adb431de223..de8990ddf4b 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -235,7 +235,7 @@ {"name":"gitlab-labkit","version":"0.37.0","platform":"ruby","checksum":"d2dd0a60db2149a9a8eebf2975dc23f54ac3ceb01bdba732eb1b26b86dfffa70"}, {"name":"gitlab-license","version":"2.6.0","platform":"ruby","checksum":"2c1f8ae73835640ec77bf758c1d0c9730635043c01cf77902f7976e826d7d016"}, {"name":"gitlab-mail_room","version":"0.0.27","platform":"ruby","checksum":"05c07db892094cf5747ea00afb0a95c5a5406e05f34ae779f4388f2ddf962316"}, -{"name":"gitlab-markup","version":"1.10.0","platform":"ruby","checksum":"668c8058b80cbfecda10a8e6e123de0336df7f16e34662dc8eae69f597cd468d"}, +{"name":"gitlab-markup","version":"2.0.0","platform":"ruby","checksum":"951a1c871463a8f329e6c002b2da337cd547febcc1e33d84df4a212419fba02e"}, {"name":"gitlab-net-dns","version":"0.10.0","platform":"ruby","checksum":"73b4613d8c851480b7b4e631f117bce4bbb4b6b8073ecf4eb167407e46097c6e"}, {"name":"gitlab-sdk","version":"0.3.1","platform":"ruby","checksum":"48ba49084f4ab92df7c7ef9f347020d9dfdf6ed9c1e782b67264e98ffe6ea710"}, {"name":"gitlab-secret_detection","version":"0.19.0","platform":"ruby","checksum":"995d87ef652dec742de8af5015018f975a01c8961a7c71892e9be19417215613"}, diff --git a/Gemfile.lock b/Gemfile.lock index 5b65de7b52f..81daa188835 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -786,7 +786,7 @@ GEM oauth2 (>= 1.4.4, < 3) redis (>= 5, < 6) redis-namespace (>= 1.8.2) - gitlab-markup (1.10.0) + gitlab-markup (2.0.0) gitlab-net-dns (0.10.0) gitlab-sdk (0.3.1) activesupport (>= 5.2.0) @@ -2120,7 +2120,7 @@ DEPENDENCIES gitlab-labkit (~> 0.37.0) gitlab-license (~> 2.6) gitlab-mail_room (~> 0.0.24) - gitlab-markup (~> 1.10.0) + gitlab-markup (~> 2.0.0) gitlab-net-dns (~> 0.10.0) gitlab-rspec! gitlab-rspec_flaky! diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index ff1ad0a160a..e4edb482ec8 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -235,7 +235,7 @@ {"name":"gitlab-labkit","version":"0.37.0","platform":"ruby","checksum":"d2dd0a60db2149a9a8eebf2975dc23f54ac3ceb01bdba732eb1b26b86dfffa70"}, {"name":"gitlab-license","version":"2.6.0","platform":"ruby","checksum":"2c1f8ae73835640ec77bf758c1d0c9730635043c01cf77902f7976e826d7d016"}, {"name":"gitlab-mail_room","version":"0.0.27","platform":"ruby","checksum":"05c07db892094cf5747ea00afb0a95c5a5406e05f34ae779f4388f2ddf962316"}, -{"name":"gitlab-markup","version":"1.10.0","platform":"ruby","checksum":"668c8058b80cbfecda10a8e6e123de0336df7f16e34662dc8eae69f597cd468d"}, +{"name":"gitlab-markup","version":"2.0.0","platform":"ruby","checksum":"951a1c871463a8f329e6c002b2da337cd547febcc1e33d84df4a212419fba02e"}, {"name":"gitlab-net-dns","version":"0.10.0","platform":"ruby","checksum":"73b4613d8c851480b7b4e631f117bce4bbb4b6b8073ecf4eb167407e46097c6e"}, {"name":"gitlab-sdk","version":"0.3.1","platform":"ruby","checksum":"48ba49084f4ab92df7c7ef9f347020d9dfdf6ed9c1e782b67264e98ffe6ea710"}, {"name":"gitlab-secret_detection","version":"0.19.0","platform":"ruby","checksum":"995d87ef652dec742de8af5015018f975a01c8961a7c71892e9be19417215613"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index 699a2f93827..0157abab51c 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -798,7 +798,7 @@ GEM oauth2 (>= 1.4.4, < 3) redis (>= 5, < 6) redis-namespace (>= 1.8.2) - gitlab-markup (1.10.0) + gitlab-markup (2.0.0) gitlab-net-dns (0.10.0) gitlab-sdk (0.3.1) activesupport (>= 5.2.0) @@ -2154,7 +2154,7 @@ DEPENDENCIES gitlab-labkit (~> 0.37.0) gitlab-license (~> 2.6) gitlab-mail_room (~> 0.0.24) - gitlab-markup (~> 1.10.0) + gitlab-markup (~> 2.0.0) gitlab-net-dns (~> 0.10.0) gitlab-rspec! gitlab-rspec_flaky! diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue index d074591502b..c80a98ac147 100644 --- a/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue +++ b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue @@ -145,7 +145,6 @@ export default { diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue b/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue deleted file mode 100644 index eee9023ba08..00000000000 --- a/app/assets/javascripts/search/sidebar/components/label_filter/label_dropdown_items.vue +++ /dev/null @@ -1,45 +0,0 @@ - - diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index fa8b5c4b235..6ec62116981 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -639,9 +639,7 @@ export default { variant="danger" @action="handleDelete" > - + diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss index 7ea2e4682a9..9344702ee21 100644 --- a/app/assets/stylesheets/page_bundles/search.scss +++ b/app/assets/stylesheets/page_bundles/search.scss @@ -89,10 +89,6 @@ $black-divider: #666; .custom-control-label { display: flex; margin-bottom: 0; - - .label-title { - margin-left: -$gl-spacing-scale-2; - } } } } diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb index 170a7d74063..e00ba9a66b6 100644 --- a/app/graphql/graphql_triggers.rb +++ b/app/graphql/graphql_triggers.rb @@ -1,6 +1,12 @@ # frozen_string_literal: true module GraphqlTriggers + def self.ci_pipeline_status_updated(pipeline) + return unless Feature.enabled?(:ci_pipeline_status_realtime, pipeline.project) + + GitlabSchema.subscriptions.trigger(:ci_pipeline_status_updated, { pipeline_id: pipeline.to_gid }, pipeline) + end + def self.issuable_assignees_updated(issuable) GitlabSchema.subscriptions.trigger(:issuable_assignees_updated, { issuable_id: issuable.to_gid }, issuable) end diff --git a/app/graphql/subscriptions/ci/pipelines/status_updated.rb b/app/graphql/subscriptions/ci/pipelines/status_updated.rb new file mode 100644 index 00000000000..cce03f5bc64 --- /dev/null +++ b/app/graphql/subscriptions/ci/pipelines/status_updated.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Subscriptions # rubocop:disable Gitlab/BoundedContexts -- Existing module + module Ci + module Pipelines + class StatusUpdated < ::Subscriptions::BaseSubscription + include Gitlab::Graphql::Laziness + + argument :pipeline_id, ::Types::GlobalIDType[::Ci::Pipeline], + required: true, + description: 'Global ID of the pipeline.' + + payload_type Types::Ci::PipelineType + + def authorized?(pipeline_id:) + pipeline = force(GitlabSchema.find_by_gid(pipeline_id)) + + unauthorized! unless pipeline && Ability.allowed?(current_user, :read_pipeline, pipeline) + + true + end + end + end + end +end diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb index bee04076c0c..5ab37202344 100644 --- a/app/graphql/types/subscription_type.rb +++ b/app/graphql/types/subscription_type.rb @@ -4,6 +4,11 @@ module Types class SubscriptionType < ::Types::BaseObject graphql_name 'Subscription' + field :ci_pipeline_status_updated, + subscription: Subscriptions::Ci::Pipelines::StatusUpdated, null: true, + description: 'Triggered when a pipeline status is updated.', + experiment: { milestone: '17.10' } + field :issuable_assignees_updated, subscription: Subscriptions::IssuableUpdated, null: true, description: 'Triggered when the assignees of an issuable are updated.' diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index c84ceb3d440..9dadef6bd3b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -304,6 +304,8 @@ module Ci end after_transition do |pipeline, transition| + GraphqlTriggers.ci_pipeline_status_updated(pipeline) + next if transition.loopback? pipeline.run_after_commit do diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 476b5ef2507..c69ca83e6ba 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -178,9 +178,13 @@ module Projects # compare the inconsistency rates of both approaches, we still run # AuthorizedProjectsWorker but with some delay and lower urgency as a # safety net. - @project.group.refresh_members_authorized_projects( - priority: UserProjectAccessChangedService::LOW_PRIORITY - ) + if Feature.enabled?(:project_authorizations_update_in_background, @project.group.root_ancestor) + AuthorizedProjectUpdate::EnqueueGroupMembersRefreshAuthorizedProjectsWorker.perform_async(@project.group.id) + else + @project.group.refresh_members_authorized_projects( + priority: UserProjectAccessChangedService::LOW_PRIORITY + ) + end else owner_user = @project.namespace.owner owner_member = @project.add_owner(owner_user, current_user: current_user) diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 4d3d0fa852d..0f94fa3d34d 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -13,6 +13,16 @@ :idempotent: true :tags: [] :queue_namespace: :activity_pub +- :name: authorized_project_update:authorized_project_update_enqueue_group_members_refresh_authorized_projects + :worker_name: AuthorizedProjectUpdate::EnqueueGroupMembersRefreshAuthorizedProjectsWorker + :feature_category: :permissions + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] + :queue_namespace: :authorized_project_update - :name: authorized_project_update:authorized_project_update_project_recalculate :worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker :feature_category: :permissions diff --git a/app/workers/authorized_project_update/enqueue_group_members_refresh_authorized_projects_worker.rb b/app/workers/authorized_project_update/enqueue_group_members_refresh_authorized_projects_worker.rb new file mode 100644 index 00000000000..a68001eb07d --- /dev/null +++ b/app/workers/authorized_project_update/enqueue_group_members_refresh_authorized_projects_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate # rubocop:disable Gitlab/BoundedContexts -- keeping related workers in the same module + class EnqueueGroupMembersRefreshAuthorizedProjectsWorker + include ApplicationWorker + + sidekiq_options retry: 3 + feature_category :permissions + urgency :low + data_consistency :delayed + queue_namespace :authorized_project_update + + idempotent! + deduplicate :until_executing, including_scheduled: true + + def perform(group_id) + group = Group.find_by_id(group_id) + return unless group + + group.refresh_members_authorized_projects( + priority: UserProjectAccessChangedService::LOW_PRIORITY + ) + end + end +end diff --git a/config/feature_flags/development/glql_integration.yml b/config/feature_flags/development/glql_integration.yml index 9ad66fdc2ce..8d041b20ace 100644 --- a/config/feature_flags/development/glql_integration.yml +++ b/config/feature_flags/development/glql_integration.yml @@ -6,4 +6,4 @@ feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/14767 milestone: '17.3' type: development group: group::knowledge -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/gitlab_com_derisk/ci_pipeline_status_realtime.yml b/config/feature_flags/gitlab_com_derisk/ci_pipeline_status_realtime.yml new file mode 100644 index 00000000000..42e2fcbe2d2 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/ci_pipeline_status_realtime.yml @@ -0,0 +1,9 @@ +--- +name: ci_pipeline_status_realtime +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516247 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183867 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/524707 +milestone: '17.10' +group: group::pipeline execution +type: gitlab_com_derisk +default_enabled: false diff --git a/config/feature_flags/gitlab_com_derisk/project_authorizations_update_in_background.yml b/config/feature_flags/gitlab_com_derisk/project_authorizations_update_in_background.yml new file mode 100644 index 00000000000..0027b5b5fec --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/project_authorizations_update_in_background.yml @@ -0,0 +1,9 @@ +--- +name: project_authorizations_update_in_background +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/523919 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183920 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/523975 +milestone: '17.10' +group: group::authorization +type: gitlab_com_derisk +default_enabled: false diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 7f2dfcbdcf0..560a0938521 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -79,6 +79,8 @@ - 1 - - authorized_keys - 2 +- - authorized_project_update:authorized_project_update_enqueue_group_members_refresh_authorized_projects + - 1 - - authorized_project_update:authorized_project_update_project_recalculate - 1 - - authorized_project_update:authorized_project_update_project_recalculate_per_user diff --git a/doc/administration/postgresql/replication_and_failover.md b/doc/administration/postgresql/replication_and_failover.md index e34687729f7..be4ee1d0c40 100644 --- a/doc/administration/postgresql/replication_and_failover.md +++ b/doc/administration/postgresql/replication_and_failover.md @@ -84,13 +84,6 @@ You also need to take into consideration the underlying network topology, making sure you have redundant connectivity between all Database and GitLab instances to avoid the network becoming a single point of failure. -{{< alert type="note" >}} - -PostgreSQL 12 is shipped with Linux package installations. Clustering for PostgreSQL 12 is supported only with -Patroni, and thus Patroni becomes mandatory for replication and failover. See the [Patroni](#patroni) section for further details. - -{{< /alert >}} - ### Database node Each database node runs four services: diff --git a/doc/subscriptions/gitlab_dedicated/data_residency_and_high_availability.md b/doc/subscriptions/gitlab_dedicated/data_residency_and_high_availability.md index 14a2c27b1ec..9d2f1321fa7 100644 --- a/doc/subscriptions/gitlab_dedicated/data_residency_and_high_availability.md +++ b/doc/subscriptions/gitlab_dedicated/data_residency_and_high_availability.md @@ -27,26 +27,70 @@ GitLab Dedicated is available in select AWS regions that meet specific requireme The following regions are verified for use: -- Asia Pacific (Mumbai) -- Asia Pacific (Seoul) -- Asia Pacific (Singapore) -- Asia Pacific (Sydney) -- Asia Pacific (Tokyo) -- Canada (Central) -- Europe (Frankfurt) -- Europe (Ireland) -- Europe (London) -- Europe (Stockholm) -- US East (Ohio) -- US East (N. Virginia) -- US West (N. California) -- US West (Oregon) -- Middle East (Bahrain) +| Region | Code | +|--------|------| +| Asia Pacific (Mumbai) | `ap-south-1` | +| Asia Pacific (Seoul) | `ap-northeast-2` | +| Asia Pacific (Singapore) | `ap-southeast-1` | +| Asia Pacific (Sydney) | `ap-southeast-2` | +| Asia Pacific (Tokyo) | `ap-northeast-1` | +| Canada (Central) | `ca-central-1` | +| Europe (Frankfurt) | `eu-central-1` | +| Europe (Ireland) | `eu-west-1` | +| Europe (London) | `eu-west-2` | +| Europe (Stockholm) | `eu-north-1` | +| US East (Ohio) | `us-east-2` | +| US East (N. Virginia) | `us-east-1` | +| US West (N. California) | `us-west-1` | +| US West (Oregon) | `us-west-2` | +| Middle East (Bahrain) | `me-south-1` | For more information about selecting low emission regions, see [Choose Region based on both business requirements and sustainability goals](https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sus_sus_region_a2.html). If you're interested in a region not listed here, contact your account representative or [GitLab Support](https://about.gitlab.com/support/) to inquire about availability. +### Secondary regions with limited support + +When setting up GitLab Dedicated, you select a secondary region to host a failover instance for +disaster recovery. Some AWS regions are available only as secondary regions because they do not fully support certain AWS +features that GitLab Dedicated relies on. If GitLab initiates a failover to your secondary region during +a disaster recovery event or test, these limitations may impact available features. + +The following regions are verified for use as a secondary region but with known limitations: + +| Region | Code | io2 volume support| AWS SES support | +|--------|------|-------------------|------------| +| Africa (Cape Town) | `af-south-1` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | +| Asia Pacific (Hong Kong) | `ap-east-1` | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No | +| Asia Pacific (Osaka) | `ap-northeast-3` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | +| Asia Pacific (Hyderabad) | `ap-south-2` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Asia Pacific (Jakarta) | `ap-southeast-3` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | +| Asia Pacific (Melbourne) | `ap-southeast-4` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Asia Pacific (Malaysia) | `ap-southeast-5` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Asia Pacific (Thailand) | `ap-southeast-7` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Canada West (Calgary) | `ca-west-1` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Europe (Zurich) | `eu-central-2` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Europe (Milan) | `eu-south-1` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | +| Europe (Spain) | `eu-south-2` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Europe (Paris) | `eu-west-3` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | +| Israel (Tel Aviv) | `il-central-1` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | +| Middle East (UAE) | `me-central-1` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| Mexico (Central) | `mx-central-1` | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | +| South America (São Paulo) | `sa-east-1` | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | + +These limitations may affect your service in the following ways: + +- **No io2 volume support**: Regions without io2 volume support use gp3 volumes instead, which offer lower + data durability (99.8-99.9% compared to 99.999% for io2), potentially longer [RTO and RPO](https://handbook.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/slas/) recovery times, and + may affect failover capabilities if rebuilding is necessary. + +- **No SES support**: Regions without AWS Simple Email Service (SES) support cannot send email + notifications using the default configuration. To maintain email functionality in these regions, + you must set up your own [external SMTP mail service](../../administration/dedicated/configure_instance/users_notifications.md#smtp-email-service). + +During onboarding, regions with these limitations are clearly marked. You must acknowledge the +associated risks before selecting one as your secondary region. + ## Data isolation As a single-tenant SaaS solution, GitLab Dedicated provides infrastructure-level isolation: @@ -77,6 +121,12 @@ For more information, see the [Current Service Level Objective](https://handbook During [onboarding](../../administration/dedicated/create_instance.md#step-2-create-your-gitlab-dedicated-instance), you specify a secondary AWS region for data storage and recovery. Regular backups of all GitLab Dedicated datastores (including databases and Git repositories) are taken, tested, and stored in your chosen secondary region. +{{< alert type="note" >}} + +Some secondary regions have [limited support](#secondary-regions-with-limited-support) for AWS features. These limitations may affect disaster recovery time frames and certain features in your failover instance. + +{{< /alert >}} + You can also opt to store backup copies in a separate cloud region for increased redundancy. For more information, including Recovery Point Objective (RPO) and Recovery Time Objective (RTO) targets, see the [disaster recovery plan](https://handbook.gitlab.com/handbook/engineering/infrastructure/team/gitlab-dedicated/slas/#disaster-recovery-plan). diff --git a/doc/user/glql/_index.md b/doc/user/glql/_index.md index 990ac22f3b8..620100a7571 100644 --- a/doc/user/glql/_index.md +++ b/doc/user/glql/_index.md @@ -8,8 +8,8 @@ title: GitLab Query Language (GLQL) {{< details >}} - Tier: Free, Premium, Ultimate -- Offering: GitLab.com, GitLab Self-Managed -- Status: Experiment +- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated +- Status: Beta {{< /details >}} @@ -17,7 +17,8 @@ title: GitLab Query Language (GLQL) - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/14767) in GitLab 17.4 [with a flag](../../administration/feature_flags.md) named `glql_integration`. Disabled by default. - Enabled on GitLab.com in GitLab 17.4 for a subset of groups and projects. -- `iteration` and `cadence` fields [introduced](https://gitlab.com/gitlab-org/gitlab-query-language/gitlab-query-language/-/issues/74) in GitLab 17.6. +- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/476990) from experiment to beta in GitLab 17.10. +- Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in GitLab 17.10. {{< /history >}} diff --git a/doc/user/glql/fields.md b/doc/user/glql/fields.md index 26a504c7e80..573adaebf79 100644 --- a/doc/user/glql/fields.md +++ b/doc/user/glql/fields.md @@ -8,8 +8,8 @@ title: GLQL fields {{< details >}} - Tier: Free, Premium, Ultimate -- Offering: GitLab.com, GitLab Self-Managed -- Status: Experiment +- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated +- Status: Beta {{< /details >}} @@ -17,7 +17,9 @@ title: GLQL fields - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/14767) in GitLab 17.4 [with a flag](../../administration/feature_flags.md) named `glql_integration`. Disabled by default. - Enabled on GitLab.com in GitLab 17.4 for a subset of groups and projects. - +- Promoted to [beta](../../policy/development_stages_support.md#beta) status in GitLab 17.10. +- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/476990) from experiment to beta in GitLab 17.10. +- Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in GitLab 17.10. {{< /history >}} {{< alert type="flag" >}} diff --git a/doc/user/glql/functions.md b/doc/user/glql/functions.md index 7efc793c5c4..4b076bec73a 100644 --- a/doc/user/glql/functions.md +++ b/doc/user/glql/functions.md @@ -8,8 +8,8 @@ title: GLQL functions {{< details >}} - Tier: Free, Premium, Ultimate -- Offering: GitLab.com, GitLab Self-Managed -- Status: Experiment +- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated +- Status: Beta {{< /details >}} @@ -17,8 +17,9 @@ title: GLQL functions - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/14767) in GitLab 17.4 [with a flag](../../administration/feature_flags.md) named `glql_integration`. Disabled by default. - Enabled on GitLab.com in GitLab 17.4 for a subset of groups and projects. -- `iteration` and `cadence` fields [introduced](https://gitlab.com/gitlab-org/gitlab-query-language/gitlab-query-language/-/issues/74) in GitLab 17.6. - +- Promoted to [beta](../../policy/development_stages_support.md#beta) status in GitLab 17.10. +- [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/476990) from experiment to beta in GitLab 17.10. +- Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated in GitLab 17.10. {{< /history >}} {{< alert type="flag" >}} diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 0c035100b76..2d6a21a2150 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -497,7 +497,7 @@ Group permissions for [group features](group/_index.md): | View group [audit events](compliance/audit_events.md) | | | | ✓ | ✓ | ✓ | Developers and Maintainers can only view events based on their individual actions. For more details, see the [prerequisites](compliance/audit_events.md#prerequisites). | | Create project in group | | | | ✓ | ✓ | ✓ | Developers, Maintainers and Owners: Only if the project creation role is set [for the instance](../administration/settings/visibility_and_access_controls.md#define-which-roles-can-create-projects) or [for the group](group/_index.md#specify-who-can-add-projects-to-a-group).

Developers: Developers can push commits to the default branch of a new project only if the [default branch protection](group/manage.md#change-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected". | | Create subgroup | | | | | ✓ | ✓ | Maintainers: Only if users with the Maintainer role [can create subgroups](group/subgroups/_index.md#change-who-can-create-subgroups). | -| Change custom settings for the [project integrations](project/integrations/_index.md) | | | | | ✓ | ✓ | | +| Change custom settings for [project integrations](project/integrations/_index.md) | | | | | | ✓ | | | Edit [epic](group/epics/_index.md) comments (posted by any user) | | ✓ | | | ✓ | ✓ | | | Fork project into a group | | | | | ✓ | ✓ | | | View [Billing](../subscriptions/gitlab_com/_index.md#view-gitlabcom-subscription) | | | | | | ✓ | Does not apply to subgroups | diff --git a/doc/user/project/integrations/_index.md b/doc/user/project/integrations/_index.md index de4f612ed4d..f798544ab56 100644 --- a/doc/user/project/integrations/_index.md +++ b/doc/user/project/integrations/_index.md @@ -34,7 +34,7 @@ You can use: Prerequisites: -- You must have at least the Maintainer role for the group. +- You must have the Owner role for the group. To manage the group default settings for a project integration: @@ -77,7 +77,7 @@ is proposed in [epic 2137](https://gitlab.com/groups/gitlab-org/-/epics/2137). Prerequisites: -- You must have at least the Maintainer role for the group. +- You must have the Owner role for the group. To remove a group default setting: @@ -108,7 +108,8 @@ To use instance or group default settings for a project integration: Prerequisites: -- You must have at least the Maintainer role for the project or group. +- You must have at least the Maintainer role for the project integration. +- You must have the Owner role for the group integration. To use custom settings for a project or group integration: diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index b36157016b5..9a03b3c14e1 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -28,7 +28,8 @@ Use a project access token to authenticate: - The project access token as the password. Project access tokens are similar to [group access tokens](../../group/settings/group_access_tokens.md) -and [personal access tokens](../../profile/personal_access_tokens.md), but project access tokens are scoped to a project, so you cannot use them to access another project's resources. +and [personal access tokens](../../profile/personal_access_tokens.md), but are scoped only to the associated project. +You cannot use project access tokens to access resources that belong to other projects. On GitLab Self-Managed instances, project access tokens are subject to the same [maximum lifetime limits](../../../administration/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens) as personal access tokens if the limit is set. diff --git a/doc/user/workspace/_index.md b/doc/user/workspace/_index.md index ffa29998b16..b244e1b1078 100644 --- a/doc/user/workspace/_index.md +++ b/doc/user/workspace/_index.md @@ -139,7 +139,7 @@ components: attributes: gl/inject-editor: true container: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images/workspaces/ubuntu-24.04:20250109224147-golang-1.23@sha256:c3d5527641bc0c6f4fbbea4bb36fe225b8e9f1df69f682c927941327312bc676" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images/workspaces/ubuntu-24.04:20250303043223-golang-1.23-docker-27.5.1@sha256:98f36ddf5d7ac53d95a270f5791ab7f50132a4cc87676e22f4f632678d8e15e1" ``` A GitLab default devfile might not be suitable for all development environments configurations. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c9ab0dc7a7d..6648f4e4cf7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -35676,16 +35676,19 @@ msgstr "" msgid "MemberInviteEmail|Invitation to join the %{project_or_group} %{project_or_group_name}" msgstr "" +msgid "MemberRole|%{adminCount} Admin" +msgstr "" + msgid "MemberRole|%{count} of %{total} permissions added" msgstr "" msgid "MemberRole|%{count} of %{total} permissions selected" msgstr "" -msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}" +msgid "MemberRole|%{defaultCount} Default %{customCount} Custom" msgstr "" -msgid "MemberRole|%{rolesStart}Roles:%{rolesEnd} %{customCount} Custom %{defaultCount} Default" +msgid "MemberRole|%{requirement} has to be enabled in order to enable %{permission}" msgstr "" msgid "MemberRole|Access level" @@ -35727,6 +35730,12 @@ msgstr "" msgid "MemberRole|Create role" msgstr "" +msgid "MemberRole|Custom admin role" +msgstr "" + +msgid "MemberRole|Custom member role" +msgstr "" + msgid "MemberRole|Custom permissions" msgstr "" diff --git a/package.json b/package.json index 4cdfff6649d..188c2746600 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@gitlab/fonts": "^1.3.0", "@gitlab/query-language-rust": "0.4.2", "@gitlab/svgs": "3.123.0", - "@gitlab/ui": "111.0.0", + "@gitlab/ui": "111.1.0", "@gitlab/vue-router-vue3": "npm:vue-router@4.5.0", "@gitlab/vuex-vue3": "npm:vuex@4.1.0", "@gitlab/web-ide": "^0.0.1-dev-20250309164831", diff --git a/spec/frontend/search/sidebar/components/label_dropdown_items_spec.js b/spec/frontend/search/sidebar/components/label_dropdown_items_spec.js deleted file mode 100644 index 95c4d1faa56..00000000000 --- a/spec/frontend/search/sidebar/components/label_dropdown_items_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { GlFormCheckbox } from '@gitlab/ui'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { PROCESS_LABELS_DATA } from 'jest/search/mock_data'; -import LabelDropdownItems from '~/search/sidebar/components/label_filter/label_dropdown_items.vue'; - -Vue.use(Vuex); - -describe('LabelDropdownItems', () => { - let wrapper; - - const defaultProps = { - labels: PROCESS_LABELS_DATA, - }; - - const createComponent = (Props = defaultProps) => { - wrapper = shallowMountExtended(LabelDropdownItems, { - propsData: { - ...Props, - }, - }); - }; - - const findAllLabelItems = () => wrapper.findAllByTestId('label-filter-menu-item'); - const findFirstLabelCheckbox = () => findAllLabelItems().at(0).findComponent(GlFormCheckbox); - const findFirstLabelTitle = () => findAllLabelItems().at(0).find('.label-title'); - const findFirstLabelColor = () => - findAllLabelItems().at(0).find('[data-testid="label-color-indicator"]'); - - describe('Renders correctly', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders items', () => { - expect(findAllLabelItems().exists()).toBe(true); - expect(findAllLabelItems()).toHaveLength(defaultProps.labels.length); - }); - - it('renders items checkbox', () => { - expect(findFirstLabelCheckbox().exists()).toBe(true); - }); - - it('renders label title', () => { - expect(findFirstLabelTitle().exists()).toBe(true); - expect(findFirstLabelTitle().text()).toBe(defaultProps.labels[0].title); - }); - - it('renders label color', () => { - expect(findFirstLabelColor().exists()).toBe(true); - expect(findFirstLabelColor().attributes('style')).toBe( - `background-color: ${defaultProps.labels[0].color};`, - ); - }); - }); -}); diff --git a/spec/frontend/search/sidebar/components/label_filter_spec.js b/spec/frontend/search/sidebar/components/label_filter_spec.js index aa1cf03a748..4d0556e8850 100644 --- a/spec/frontend/search/sidebar/components/label_filter_spec.js +++ b/spec/frontend/search/sidebar/components/label_filter_spec.js @@ -1,13 +1,5 @@ -import { - GlAlert, - GlLoadingIcon, - GlSearchBoxByType, - GlLabel, - GlDropdownForm, - GlFormCheckboxGroup, - GlDropdownSectionHeader, - GlDropdownDivider, -} from '@gitlab/ui'; +import { GlLabel, GlCollapsibleListbox } from '@gitlab/ui'; + import Vue, { nextTick } from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; @@ -21,18 +13,12 @@ import { import LabelFilter from '~/search/sidebar/components/label_filter/index.vue'; -import LabelDropdownItems from '~/search/sidebar/components/label_filter/label_dropdown_items.vue'; - import * as actions from '~/search/store/actions'; import * as getters from '~/search/store/getters'; import mutations from '~/search/store/mutations'; import createState from '~/search/store/state'; -import { - RECEIVE_AGGREGATIONS_SUCCESS, - REQUEST_AGGREGATIONS, - RECEIVE_AGGREGATIONS_ERROR, -} from '~/search/store/mutation_types'; +import { RECEIVE_AGGREGATIONS_SUCCESS } from '~/search/store/mutation_types'; Vue.use(Vuex); @@ -86,20 +72,43 @@ describe('GlobalSearchSidebarLabelFilter', () => { const findComponentTitle = () => wrapper.findByTestId('label-filter-title'); const findAllSelectedLabelsAbove = () => wrapper.findAllComponents(GlLabel); - const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); - const findDropdownForm = () => wrapper.findComponent(GlDropdownForm); - const findCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup); - const findDropdownSectionHeader = () => wrapper.findComponent(GlDropdownSectionHeader); - const findDivider = () => wrapper.findComponent(GlDropdownDivider); - const findCheckboxFilter = () => wrapper.findAllComponents(LabelDropdownItems); - const findAlert = () => wrapper.findComponent(GlAlert); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findNoLabelsFoundMessage = () => wrapper.findByTestId('no-labels-found-message'); + const findCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); const findLabelPills = () => wrapper.findAllComponentsByTestId('label'); - const findSelectedUappliedLavelPills = () => wrapper.findAllComponentsByTestId('unapplied-label'); + const findSelectedUnappliedLabelPills = () => + wrapper.findAllComponentsByTestId('unapplied-label'); const findClosedUnappliedPills = () => wrapper.findAllComponentsByTestId('unselected-label'); - const findSelectedLabelsCheckboxes = () => wrapper.findByTestId('selected-labels-checkboxes'); + + describe('Renders correctly opened', () => { + beforeEach(async () => { + createComponent(); + store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); + + await nextTick(); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await nextTick(); + findCollapsibleListbox().vm.open(); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('renders component title', () => { + expect(findComponentTitle().exists()).toBe(true); + }); + + it('renders selected labels above search box', () => { + expect(findAllSelectedLabelsAbove().exists()).toBe(true); + expect(findAllSelectedLabelsAbove()).toHaveLength(2); + }); + + it('sends tracking information when dropdown is opened', () => { + expect(trackingSpy).toHaveBeenCalledWith('search:agreggations:label:show', 'Dropdown', { + label: 'Dropdown', + }); + }); + }); describe('Renders correctly closed', () => { beforeEach(async () => { @@ -118,170 +127,8 @@ describe('GlobalSearchSidebarLabelFilter', () => { expect(findAllSelectedLabelsAbove()).toHaveLength(2); }); - it('renders search box', () => { - expect(findSearchBox().exists()).toBe(true); - }); - - it("doesn't render dropdown form", () => { - expect(findDropdownForm().exists()).toBe(false); - }); - - it("doesn't render checkbox group", () => { - expect(findCheckboxGroup().exists()).toBe(false); - }); - - it("doesn't render dropdown section header", () => { - expect(findDropdownSectionHeader().exists()).toBe(false); - }); - - it("doesn't render divider", () => { - expect(findDivider().exists()).toBe(false); - }); - - it("doesn't render checkbox filter", () => { - expect(findCheckboxFilter().exists()).toBe(false); - }); - - it("doesn't render alert", () => { - expect(findAlert().exists()).toBe(false); - }); - - it("doesn't render loading icon", () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('Renders correctly opened', () => { - beforeEach(async () => { - createComponent(); - store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); - - await nextTick(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - findSearchBox().vm.$emit('focusin'); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('renders component title', () => { - expect(findComponentTitle().exists()).toBe(true); - }); - - it('renders selected labels above search box', () => { - // default data need to provide at least two selected labels - expect(findAllSelectedLabelsAbove().exists()).toBe(true); - expect(findAllSelectedLabelsAbove()).toHaveLength(2); - }); - - it('renders search box', () => { - expect(findSearchBox().exists()).toBe(true); - }); - - it('renders dropdown form', () => { - expect(findDropdownForm().exists()).toBe(true); - }); - - it('renders checkbox group', () => { - expect(findCheckboxGroup().exists()).toBe(true); - }); - - it('renders dropdown section header', () => { - expect(findDropdownSectionHeader().exists()).toBe(true); - }); - - it('renders divider', () => { - expect(findDivider().exists()).toBe(true); - }); - - it('renders checkbox filter', () => { - expect(findCheckboxFilter().exists()).toBe(true); - }); - - it("doesn't render alert", () => { - expect(findAlert().exists()).toBe(false); - }); - - it("doesn't render loading icon", () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('sends tracking information when dropdown is opened', () => { - expect(trackingSpy).toHaveBeenCalledWith('search:agreggations:label:show', 'Dropdown', { - label: 'Dropdown', - }); - }); - }); - - describe('Renders loading state correctly', () => { - beforeEach(async () => { - createComponent(); - store.commit(REQUEST_AGGREGATIONS); - await Vue.nextTick(); - - findSearchBox().vm.$emit('focusin'); - }); - - it('renders checkbox filter', () => { - expect(findCheckboxFilter().exists()).toBe(false); - }); - - it("doesn't render alert", () => { - expect(findAlert().exists()).toBe(false); - }); - - it('renders loading icon', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('Renders no-labels state correctly', () => { - beforeEach(async () => { - createComponent(); - store.commit(REQUEST_AGGREGATIONS); - await Vue.nextTick(); - - findSearchBox().vm.$emit('focusin'); - findSearchBox().vm.$emit('input', 'ssssssss'); - }); - - it('renders checkbox filter', () => { - expect(findCheckboxFilter().exists()).toBe(false); - }); - - it("doesn't render alert", () => { - expect(findAlert().exists()).toBe(false); - }); - - it("doesn't render items", () => { - expect(findAllSelectedLabelsAbove().exists()).toBe(false); - }); - - it('renders no labels found text', () => { - expect(findNoLabelsFoundMessage().exists()).toBe(true); - }); - }); - - describe('Renders error state correctly', () => { - beforeEach(async () => { - createComponent(); - store.commit(RECEIVE_AGGREGATIONS_ERROR); - await Vue.nextTick(); - - findSearchBox().vm.$emit('focusin'); - }); - - it("doesn't render checkbox filter", () => { - expect(findCheckboxFilter().exists()).toBe(false); - }); - - it('renders alert', () => { - expect(findAlert().exists()).toBe(true); - }); - - it("doesn't render loading icon", () => { - expect(findLoadingIcon().exists()).toBe(false); + it('renders search dropdown', () => { + expect(findCollapsibleListbox().exists()).toBe(true); }); }); @@ -297,26 +144,13 @@ describe('GlobalSearchSidebarLabelFilter', () => { }); }); - describe('Closing label works correctly', () => { - beforeEach(async () => { - createComponent(); - store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); - await Vue.nextTick(); - }); - - it('renders checkbox filter', async () => { - await findAllSelectedLabelsAbove().at(0).find('.btn-reset').trigger('click'); - expect(actionSpies.closeLabel).toHaveBeenCalled(); - }); - }); - describe('label search input box works properly', () => { beforeEach(() => { createComponent(); }); it('renders checkbox filter', () => { - findSearchBox().find('input').setValue('test'); + findCollapsibleListbox().vm.$emit('search', 'test'); expect(actionSpies.setLabelFilterSearch).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ @@ -326,26 +160,17 @@ describe('GlobalSearchSidebarLabelFilter', () => { }); }); - describe('dropdown checkboxes work', () => { + describe('when selecting', () => { + let mockValueForSelecting; + beforeEach(async () => { createComponent(); - store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); - await Vue.nextTick(); - - await findSearchBox().vm.$emit('focusin'); - await Vue.nextTick(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - await findCheckboxGroup().vm.$emit('input', [6]); + store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); await Vue.nextTick(); - }); - - it('trigger event', () => { - expect(actionSpies.setQuery).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ key: 'label_name', value: ['Cosche'] }), - ); + mockValueForSelecting = findCollapsibleListbox().vm.items[2].value; + findCollapsibleListbox().vm.$emit('select', mockValueForSelecting); }); it('sends tracking information when checkbox is selected', () => { @@ -354,7 +179,7 @@ describe('GlobalSearchSidebarLabelFilter', () => { 'Label Checkbox', { label: 'Label Key', - property: [6], + property: mockValueForSelecting, }, ); }); @@ -367,7 +192,7 @@ describe('GlobalSearchSidebarLabelFilter', () => { }); it('has correct pills', () => { - expect(findSelectedUappliedLavelPills()).toHaveLength(2); + expect(findSelectedUnappliedLabelPills()).toHaveLength(2); }); }); @@ -384,44 +209,6 @@ describe('GlobalSearchSidebarLabelFilter', () => { }); }); - describe('when applied labels show as slected in dropdown', () => { - beforeEach(() => { - const mockGetters = { - appliedSelectedLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS), - }; - createComponent({}, mockGetters); - store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); - }); - - it('has correct checkboxes', async () => { - await findSearchBox().vm.$emit('focusin'); - - expect(findSelectedLabelsCheckboxes().findAll('li')).toHaveLength(2); - }); - }); - - describe('when searching labels', () => { - beforeEach(() => { - const mockGetters = { - appliedSelectedLabels: jest.fn(() => MOCK_FILTERED_UNSELECTED_LABELS), - }; - createComponent({ searchLabelString: 'Cosche' }, mockGetters); - store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data); - }); - - it('correctly merges selected and newly selected labels', async () => { - await findSearchBox().vm.$emit('focusin'); - await findCheckboxGroup().vm.$emit('input', [6]); - await Vue.nextTick(); - - expect(store.state.query.label_name).toEqual(['Aftersync', 'Brist']); - expect(actionSpies.setQuery.mock.lastCall[1]).toEqual({ - key: 'label_name', - value: ['Cosche'], - }); - }); - }); - describe('closed unapplied labels show as pills above dropdown', () => { beforeEach(() => { const mockGetters = { diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb index 57ae31c46ac..aaf59e3dfcb 100644 --- a/spec/graphql/graphql_triggers_spec.rb +++ b/spec/graphql/graphql_triggers_spec.rb @@ -217,4 +217,35 @@ RSpec.describe GraphqlTriggers, feature_category: :shared do end end end + + describe '.ci_pipeline_status_updated' do + let_it_be(:pipeline) { create(:ci_pipeline) } + let_it_be(:user) { pipeline.project.owners.first } + + it 'triggers the ci_pipeline_status_updated subscription' do + expect(GitlabSchema.subscriptions).to receive(:trigger).with( + :ci_pipeline_status_updated, + { pipeline_id: pipeline.to_gid }, + pipeline + ).and_call_original + + described_class.ci_pipeline_status_updated(pipeline) + end + + describe 'when ci_pipeline_status_realtime is disabled' do + before do + stub_feature_flags(ci_pipeline_status_realtime: false) + end + + it 'does not trigger the ci_pipeline_status_updated subscription' do + expect(GitlabSchema.subscriptions).not_to receive(:trigger).with( + :ci_pipeline_status_updated, + { pipeline_id: pipeline.to_gid }, + pipeline + ).and_call_original + + described_class.ci_pipeline_status_updated(pipeline) + end + end + end end diff --git a/spec/graphql/subscriptions/ci/pipelines/status_updated_spec.rb b/spec/graphql/subscriptions/ci/pipelines/status_updated_spec.rb new file mode 100644 index 00000000000..ae792a23960 --- /dev/null +++ b/spec/graphql/subscriptions/ci/pipelines/status_updated_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Subscriptions::Ci::Pipelines::StatusUpdated, feature_category: :continuous_integration do + include GraphqlHelpers + + it { expect(described_class).to have_graphql_arguments(:pipeline_id) } + it { expect(described_class.payload_type).to eq(Types::Ci::PipelineType) } + + describe '#resolve' do + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:current_user) { pipeline.project.owners.first } + let(:pipeline_id) { pipeline.to_gid } + + subject(:subscription) { resolver.resolve_with_support(pipeline_id: pipeline_id) } + + context 'when initially subscribing to the pipeline' do + let(:resolver) { resolver_instance(described_class, ctx: query_context, subscription_update: false) } + + it 'returns nil' do + expect(subscription).to be_nil + end + + context 'when the user is unauthorized' do + let(:current_user) { unauthorized_user } + + it 'raises an exception' do + expect { subscription }.to raise_error(GraphQL::ExecutionError) + end + end + + context 'when the pipeline does not exist' do + let(:pipeline_id) { GlobalID.parse("gid://gitlab/Ci::Pipeline/#{non_existing_record_id}") } + + it 'raises an exception' do + expect { subscription }.to raise_error(GraphQL::ExecutionError) + end + end + end + + context 'with subscription updates' do + let(:resolver) do + resolver_instance(described_class, obj: pipeline, ctx: query_context, subscription_update: true) + end + + it 'returns the resolved object' do + expect(subscription).to eq(pipeline) + end + + context 'when user can not read the pipeline' do + before do + allow(Ability).to receive(:allowed?) + .with(current_user, :read_pipeline, pipeline) + .and_return(false) + end + + it 'unsubscribes the user' do + # GraphQL::Execution::Skip is returned when unsubscribed + expect(subscription).to be_an(GraphQL::Execution::Skip) + end + end + end + end +end diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb index 295420d37b7..fb9150d04bd 100644 --- a/spec/graphql/types/subscription_type_spec.rb +++ b/spec/graphql/types/subscription_type_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Subscription'], feature_category: :subscription_management do it 'has the expected fields' do expected_fields = %i[ + ci_pipeline_status_updated issuable_assignees_updated issue_crm_contacts_updated issuable_title_updated diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 8cc8dcdaf2e..aa8ac659c8c 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2250,6 +2250,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: end end + describe 'pipeline status update subscription trigger' do + %w[run! succeed! drop! skip! cancel! block! delay!].each do |action| + context "when pipeline receives #{action} event" do + it 'triggers GraphQL subscription ciPipelineStatusUpdated' do + expect(GraphqlTriggers).to receive(:ci_pipeline_status_updated).with(pipeline) + + pipeline.public_send(action) + end + end + end + end + def create_build(name, *traits, queued_at: current, started_from: 0, **opts) create( :ci_build, *traits, diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 4181bf7d7e1..62747fab2df 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -223,6 +223,38 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :groups_an root_namespace_id: group.parent_id ) end + + describe 'project authorizations refresh' do + let_it_be(:group) { create(:group, owners: user) } + + subject(:create_proj) { create_project(user, opts.merge(namespace_id: group.id)) } + + it 'enqueues a EnqueueGroupMembersRefreshAuthorizedProjectsWorker job' do + expect(AuthorizedProjectUpdate::EnqueueGroupMembersRefreshAuthorizedProjectsWorker) + .to receive(:perform_async).with(group.id) + + create_proj + end + + context 'when project_authorizations_update_in_background feature flag is disabled' do + before do + stub_feature_flags(project_authorizations_update_in_background: false) + end + + it 'calls refresh_members_authorized_projects on the project\'s group with correct params' do + expect_next_instances_of(Project, 4) do |instance| + allow(group).to receive(:refresh_members_authorized_projects) + allow(instance).to receive(:group).and_return(group) + end + + create_proj + + expect(group).to have_received(:refresh_members_authorized_projects).with( + priority: ::UserProjectAccessChangedService::LOW_PRIORITY + ) + end + end + end end context "admin creates project with other user's namespace_id" do diff --git a/spec/workers/authorized_project_update/enqueue_group_members_refresh_authorized_projects_worker_spec.rb b/spec/workers/authorized_project_update/enqueue_group_members_refresh_authorized_projects_worker_spec.rb new file mode 100644 index 00000000000..1f20f6599f4 --- /dev/null +++ b/spec/workers/authorized_project_update/enqueue_group_members_refresh_authorized_projects_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AuthorizedProjectUpdate::EnqueueGroupMembersRefreshAuthorizedProjectsWorker, feature_category: :permissions do + describe '#perform' do + context 'when group exists' do + let(:group) { create(:group) } + + it 'calls Group#refresh_members_authorized_projects' do + allow(Group).to receive(:find_by_id).with(group.id).and_return(group) + + expect(group).to receive(:refresh_members_authorized_projects).with( + priority: UserProjectAccessChangedService::LOW_PRIORITY + ) + + described_class.new.perform(group.id) + end + end + + context 'when group is not found' do + it 'does not raise errors' do + expect { described_class.new.perform(non_existing_record_id) }.not_to raise_error + end + end + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 96dc7189093..7359ce0da2c 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -132,6 +132,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'ApprovalRules::ExternalApprovalRulePayloadWorker' => 3, 'ApproveBlockedPendingApprovalUsersWorker' => 3, 'AuthorizedKeysWorker' => 3, + 'AuthorizedProjectUpdate::EnqueueGroupMembersRefreshAuthorizedProjectsWorker' => 3, 'AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' => 3, 'AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker' => 3, 'AuthorizedProjectUpdate::UserRefreshFromReplicaWorker' => 3, diff --git a/yarn.lock b/yarn.lock index 49bc28e6ec4..85294578847 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1417,8 +1417,7 @@ resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267" integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg== -"@gitlab/noop@^1.0.1", jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1": - name jackspeak +"@gitlab/noop@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== @@ -1442,10 +1441,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.123.0.tgz#1fa3b1a709755ff7c8ef67e18c0442101655ebf0" integrity sha512-yjVn+utOTIKk8d9JlvGo6EgJ4TQ+CKpe3RddflAqtsQqQuL/2MlVdtaUePybxYzWIaumFuh5LouQ6BrWyw1niQ== -"@gitlab/ui@111.0.0": - version "111.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-111.0.0.tgz#b827f61df673074d1cb0d3d074314635bcb74aff" - integrity sha512-AynSxduL6i5xIMSysKZWhXaSXYkJGZlmCpPz2gWCBy0+IX0r8giXwHfhGqNJ/VYaZCpkTVYeY1gTiFguPlCsbg== +"@gitlab/ui@111.1.0": + version "111.1.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-111.1.0.tgz#ee51b7a35bb5d98418c040568de5732c98599191" + integrity sha512-76PvfUH93eIIUkstn7qmFPf8qnya1FRY5PLzlHVJYy3NHuNrUsfBNiYkHe4QtLjD4pigPMy4iylRT5m8fMHZvQ== dependencies: "@floating-ui/dom" "1.4.3" echarts "^5.3.2" @@ -9488,6 +9487,11 @@ iterall@^1.2.1: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" + integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== + jed@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"