summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-20 12:21:30 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-20 12:21:30 +0000
commit2366f969a4b3a95e052e551cc7283a2db8d5562e (patch)
tree33ea679dad4b92048697729f68f9c606f91b32e4
parentc7eec01f1b68b2e047cdd709751cb695ab329933 (diff)
downloadgitlab-ce-2366f969a4b3a95e052e551cc7283a2db8d5562e.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review-apps/qa.gitlab-ci.yml2
-rw-r--r--.gitlab/issue_templates/Utilization group - bug.md166
-rw-r--r--.gitlab/issue_templates/Utilization group - feature.md65
-rw-r--r--.gitlab/issue_templates/Utilization group - maintenance.md69
-rw-r--r--.rubocop.yml4
-rw-r--r--.rubocop_todo/gitlab/namespaced_class.yml2
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml1
-rw-r--r--.rubocop_todo/layout/space_in_lambda_literal.yml1
-rw-r--r--.rubocop_todo/lint/unused_block_argument.yml1
-rw-r--r--.rubocop_todo/rails/inverse_of.yml1
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--.rubocop_todo/style/numbered_parameters.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js6
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue36
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue2
-rw-r--r--app/assets/javascripts/issues/list/constants.js1
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js8
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue53
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue8
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue18
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss8
-rw-r--r--app/helpers/nav_helper.rb21
-rw-r--r--app/models/integrations/apple_app_store.rb8
-rw-r--r--app/models/integrations/gitlab_slack_application.rb176
-rw-r--r--app/models/integrations/slack_workspace/api_scope.rb22
-rw-r--r--app/models/integrations/slack_workspace/integration_api_scope.rb29
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/resource_milestone_event.rb5
-rw-r--r--app/models/resource_state_event.rb4
-rw-r--r--app/models/slack_integration.rb93
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/user_detail.rb3
-rw-r--r--app/models/work_items/resource_link_event.rb2
-rw-r--r--app/services/users/upsert_credit_card_validation_service.rb5
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml11
-rw-r--r--config/feature_flags/development/use_merge_base_for_security_widget.yml8
-rw-r--r--doc/api/users.md15
-rw-r--r--doc/architecture/blueprints/cells/glossary.md106
-rw-r--r--doc/architecture/blueprints/cells/goals.md59
-rw-r--r--doc/architecture/blueprints/cells/impact.md58
-rw-r--r--doc/architecture/blueprints/cells/index.md222
-rw-r--r--doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md2
-rw-r--r--doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md2
-rw-r--r--doc/ci/jobs/ci_job_token.md5
-rw-r--r--doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md16
-rw-r--r--doc/development/value_stream_analytics.md18
-rw-r--r--doc/integration/jira/development_panel.md2
-rw-r--r--doc/policy/alpha-beta-support.md20
-rw-r--r--doc/tutorials/convert_personal_namespace_into_group.md4
-rw-r--r--doc/tutorials/move_personal_project_to_a_group.md4
-rw-r--r--doc/user/project/integrations/apple_app_store.md7
-rw-r--r--doc/user/project/labels.md2
-rw-r--r--lib/api/entities/user_preferences.rb2
-rw-r--r--lib/api/users.rb5
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/slack/api.rb27
-rw-r--r--locale/gitlab.pot128
-rw-r--r--scripts/review_apps/base-config.yaml18
-rw-r--r--spec/factories/integrations.rb23
-rw-r--r--spec/factories/slack_integrations.rb25
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js28
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js13
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js48
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js15
-rw-r--r--spec/helpers/nav_helper_spec.rb61
-rw-r--r--spec/lib/slack/api_spec.rb41
-rw-r--r--spec/models/ci/build_spec.rb5
-rw-r--r--spec/models/integrations/apple_app_store_spec.rb6
-rw-r--r--spec/models/integrations/gitlab_slack_application_spec.rb337
-rw-r--r--spec/models/integrations/slack_workspace/api_scope_spec.rb20
-rw-r--r--spec/models/merge_request_spec.rb44
-rw-r--r--spec/models/resource_milestone_event_spec.rb18
-rw-r--r--spec/models/resource_state_event_spec.rb17
-rw-r--r--spec/models/slack_integration_spec.rb147
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/requests/api/users_preferences_spec.rb5
-rw-r--r--spec/services/projects/update_service_spec.rb34
-rw-r--r--spec/services/user_preferences/update_service_spec.rb4
-rw-r--r--spec/services/users/upsert_credit_card_validation_service_spec.rb8
-rw-r--r--spec/support/matchers/snapshot_matcher.rb55
-rw-r--r--spec/support/rspec.rb5
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--tooling/lib/tooling/gettext_extractor.rb2
90 files changed, 1931 insertions, 625 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 5adf6d25eb1..8fa1044b170 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -11,7 +11,7 @@ include:
- local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml
- local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml
- project: gitlab-org/quality/pipeline-common
- ref: 3.1.2
+ ref: 3.1.3
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml
diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
index 13623a576d5..8ef86514c54 100644
--- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
@@ -1,6 +1,6 @@
include:
- project: gitlab-org/quality/pipeline-common
- ref: 3.1.2
+ ref: 3.1.3
file:
- /ci/base.gitlab-ci.yml
- /ci/allure-report.yml
diff --git a/.gitlab/issue_templates/Utilization group - bug.md b/.gitlab/issue_templates/Utilization group - bug.md
new file mode 100644
index 00000000000..03fed78189e
--- /dev/null
+++ b/.gitlab/issue_templates/Utilization group - bug.md
@@ -0,0 +1,166 @@
+<!---
+Please read this!
+
+Before opening a new issue, make sure to search for keywords in the issues
+filtered by the "regression" or "type::bug" label:
+
+- https://gitlab.com/gitlab-org/gitlab/-/merge_requests?scope=all&label_name[]=group%3A%3Autilization&label_name[]=section%3A%3Afulfillment&label_name%5B%5D=type::regression
+- https://gitlab.com/gitlab-org/gitlab/-/merge_requests?scope=all&label_name[]=group%3A%3Autilization&label_name[]=section%3A%3Afulfillment&label_name%5B%5D=type::bug
+
+and verify the issue you're about to submit isn't a duplicate.
+--->
+Utilization group: Bug Report Template
+
+## Bug Summary
+
+<!-- Provide a brief overview of the issue. What is the problem that needs to be addressed? -->
+
+## Steps to reproduce
+
+<!-- Provide a clear and detailed description of the steps needed to reproduce the bug. This should include any specific inputs, expected outputs, and observed outputs. -->
+
+1. [Step 1]
+1. [Step 2]
+1. [Step 3]
+1. [Step 4]
+1. [Step 5]
+
+## Example Project
+
+<!-- If possible, please create an example project here on GitLab.com that exhibits the problematic
+behavior, and link to it here in the bug report. If you are using an older version of GitLab, this
+will also determine whether the bug is fixed in a more recent version. -->
+
+## What is the current *bug* behavior?
+
+<!-- Describe the current behavior of the system or application in response to the actions described in the steps above. -->
+
+## What is the expected *correct* behavior?
+
+<!-- Describe the expected behavior of the system or application in response to the actions described in the steps above. -->
+
+## Reproducibility
+
+<!-- Describe how frequently the bug occurs. -->
+
+## Impact Assessment
+
+<!-- Describe the impact of this bug on the user experience and/or the product as a whole. -->
+
+## Severity
+
+<!-- Provide an assessment of the severity of the bug, based on its impact on the user experience and/or the product as a whole. -->
+
+## Environment
+
+<!-- List the relevant environment information, including the operating system, web browser, device, etc. -->
+
+## Screenshots and/or Relevant logs
+
+<!-- Include any relevant screenshots to help illustrate the bug. -->
+<!-- Paste any relevant logs - please use code blocks (```) to format console output, logs, and code
+ as it's tough to read otherwise. -->
+
+## Output of checks (GitLab.com)
+
+<!-- If you are reporting a bug on GitLab.com, uncomment below, if not, delete this section -->
+
+<!-- This bug happens on GitLab.com -->
+<!-- /label ~"reproduced on GitLab.com" -->
+
+## Results of GitLab environment info
+
+<!-- Input any relevant GitLab environment information if needed. -->
+
+<details>
+<summary>Expand for output related to GitLab environment info</summary>
+
+<pre>
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:env:info`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+
+</pre>
+</details>
+
+## Results of GitLab application Check
+
+<!-- Input any relevant GitLab application check information if needed. -->
+
+<details>
+<summary>Expand for output related to the GitLab application check</summary>
+<pre>
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
+
+</pre>
+</details>
+
+## Possible fixes
+
+<!-- If you can, link to the line of code that might be responsible for the problem. -->
+<!-- If you have any suggestions for how to fix the bug, provide them here. -->
+<!-- If you are unsure about the subtype of this bug, please check our SSOT https://about.gitlab.com/handbook/engineering/metrics/?_gl=1*920mnx*_ga*ODQ3OTI1Mjk1LjE2NzA0MDg0NjU.*_ga_ENFH3X7M5Y*MTY4MTM3OTA3My4yNzkuMS4xNjgxMzc5MTI0LjAuMC4w#work-type-classification -->
+
+/label ~"type::bug"
+/label ~"Category:Consumables Cost Management"
+/label ~"group::utilization"
+/label ~"section::fulfillment"
+
+---
+<details>
+<summary>Illustrative Description: (This is not an actual issue, but rather a sample report that demonstrates how a bug could be presented)</summary>
+## Bug Summary
+
+When attempting to log in to GitLab using a new account, the system does not recognize the account and returns an error message.
+
+## Steps to Reproduce
+
+1. Navigate to the GitLab login page.
+1. Enter the email and password for a new account.
+1. Click the "Log In" button.
+1. Observe the error message: "The email or password you entered is incorrect. Please try again."
+
+## What is the current *bug* behavior?
+
+The system does not recognize the new account and returns an error message.
+
+## What is the expected *correct* behavior?
+
+The system should recognize the new account and allow the user to log in.
+
+## Reproducibility
+
+This bug occurs consistently when attempting to log in with a new account.
+
+## Impact Assessment
+
+This bug prevents new users from accessing GitLab and may result in frustration and lost productivity.
+
+## Severity
+
+This bug is of medium severity, as it prevents new users from accessing the system, but does not affect the functionality of existing users.
+
+## Environment
+
+- Operating System: macOS Ventura
+- Browser: Google Chrome 111.0.5563.146
+
+## Screenshots and/or Relevant logs
+
+[Insert screenshot of the error message.]
+
+## Possible Fix
+
+It is unclear what may be causing this bug. Further investigation is required to identify a possible fix.
+
+</details>
diff --git a/.gitlab/issue_templates/Utilization group - feature.md b/.gitlab/issue_templates/Utilization group - feature.md
new file mode 100644
index 00000000000..57a4d4128c0
--- /dev/null
+++ b/.gitlab/issue_templates/Utilization group - feature.md
@@ -0,0 +1,65 @@
+Utilization group: Feature Template
+
+## Description
+
+<!-- As a [user or stakeholder], I want [goal or objective] so that [reason or benefit]. -->
+
+## Acceptance Criteria
+<!--
+- [ ] [Describe what must be achieved to complete this issue.]
+- [ ] [Describe another requirement needed to complete this issue.]
+- [ ] [Add additional acceptance criteria as needed.]
+ -->
+## Technical Requirements
+
+<!-- [If applicable, please list out any technical requirements for this feature/enhancement.] -->
+
+## Design Requirements
+
+<!-- [If applicable, please provide a link to the design specifications for this feature/enhancement.] -->
+
+## Impact Assessment
+
+<!-- [Please describe the impact this feature/enhancement will have on the user experience and/or the product as a whole.] -->
+
+## User Story
+
+<!-- [Provide a user story to illustrate the use case for this feature/enhancement. Include examples to help communicate the intended functionality.] -->
+/label ~"type::feature"
+/label ~"Category:Consumables Cost Management"
+/label ~"group::utilization"
+/label ~"section::fulfillment"
+
+<details>
+<summary>Illustrative Description: (This is not an actual issue, but rather a sample report that demonstrates how a feature could be presented) </summary>
+
+## Description
+
+As a developer, I want to be able to easily create and manage merge requests, so that I can collaborate effectively with my team and ensure that code changes are reviewed and merged efficiently.
+
+## Acceptance Criteria
+
+- [ ] The merge request feature should allow developers to create a new merge request from a branch.
+- [ ] The merge request feature should allow developers to assign the merge request to another team member for review.
+- [ ] The merge request feature should provide a clear and easy-to-use interface for managing merge requests.
+- [ ] The merge request feature should integrate with other GitLab features, such as issue tracking and continuous integration.
+
+## Technical Requirements
+
+- The merge request feature should be implemented using GitLab's API.
+- The merge request feature should be integrated with GitLab's existing authentication and authorization system.
+- The merge request feature should be optimized for performance and scalability.
+
+## Design Requirements
+
+- [Design specifications for this feature can be found here.](insert_design_link_here)
+
+## Impact Assessment
+
+This feature will significantly enhance the collaboration and code review process for developers using GitLab. By providing an intuitive and easy-to-use interface for managing merge requests, developers will be able to work more efficiently and effectively as a team. Additionally, integrating the merge request feature with other GitLab features will further streamline the development process.
+
+## User Story
+
+As a developer working on a new feature branch, I want to be able to create a new merge request and assign it to a team member for review, so that I can ensure that my code changes are thoroughly reviewed before being merged into the main codebase. With the new merge request feature, I can easily create a new merge request, assign it to a team member for review, and track its status throughout the review process. This will help me work more efficiently and effectively as a team, while also maintaining high code quality and reliability.
+
+</details>
diff --git a/.gitlab/issue_templates/Utilization group - maintenance.md b/.gitlab/issue_templates/Utilization group - maintenance.md
new file mode 100644
index 00000000000..e25c80e26c7
--- /dev/null
+++ b/.gitlab/issue_templates/Utilization group - maintenance.md
@@ -0,0 +1,69 @@
+Utilization Group: Maintenance Template
+
+## Description
+<!-- Briefly describe the maintenance issue. -->
+
+## Acceptance Criteria
+<!--
+- [ ] [Describe the completion requirements.]
+- [ ] [Add additional acceptance criteria as necessary.]
+ -->
+
+## Technical Requirements
+<!-- [List any technical requirements for this maintenance issue.] -->
+
+## Impact Assessment
+<!-- [Describe the impact of this maintenance issue on the user experience and/or the product as a whole.] -->
+
+## Steps to Reproduce
+<!-- [Provide detailed steps on how to reproduce the maintenance issue.] -->
+
+## Expected Results
+<!-- [Describe the expected outcome when the maintenance issue is resolved.] -->
+
+## Actual Results
+<!-- [Describe the current outcome of the maintenance issue.] -->
+
+/label ~type::maintenance
+/label ~"Category:Consumables Cost Management"
+/label ~"group::utilization"
+/label ~"section::fulfillment"
+
+<details>
+<summary>Illustrative Description: (This is not an actual maintenance issue, but rather a sample report that demonstrates how a maintenance issue could be presented) </summary>
+
+## Description
+
+The login page is taking longer than expected to load, which is impacting the user experience.
+
+## Acceptance Criteria
+
+- [ ] The login page should load in less than 3 seconds on both desktop and mobile devices.
+- [ ] The login page should be tested on different browsers to ensure compatibility.
+- [ ] The login page should not display any errors or warnings in the console.
+
+## Technical Requirements
+
+- [ ] The login page should be optimized for performance.
+- [ ] The login page should be tested on different browsers.
+- [ ] The login page should be updated to use the latest version of the authentication library.
+
+## Impact Assessment
+
+This maintenance issue is impacting the user experience by causing delays in the login process. By resolving this issue, users will be able to log in faster and have a better overall experience.
+
+## Steps to Reproduce
+
+1. Open the login page.
+1. Wait for the page to load.
+1. Measure the time it takes for the page to fully load.
+
+## Expected Results
+
+The login page should load in less than 3 seconds on both desktop and mobile devices.
+
+## Actual Results
+
+The login page is currently taking more than 5 seconds to load on desktop devices and more than 7 seconds on mobile devices. This is causing frustration and delays for users.
+
+</details>
diff --git a/.rubocop.yml b/.rubocop.yml
index 12eee5702bc..d6df58c14c3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -987,3 +987,7 @@ SidekiqLoadBalancing/WorkerDataConsistency:
Include:
- 'app/workers/**/*'
- 'ee/app/workers/**/*'
+
+# This cop is disabled for Ruby 3.0+ anyway.
+Lint/NonDeterministicRequireOrder:
+ Enabled: false
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index 0ec0bc66869..4f5d5ab3a80 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -307,6 +307,7 @@ Gitlab/NamespacedClass:
- 'app/models/service_desk_setting.rb'
- 'app/models/service_list.rb'
- 'app/models/shard.rb'
+ - 'app/models/slack_integration.rb'
- 'app/models/snippet.rb'
- 'app/models/snippet_blob.rb'
- 'app/models/snippet_input_action.rb'
@@ -938,7 +939,6 @@ Gitlab/NamespacedClass:
- 'ee/app/models/scim_identity.rb'
- 'ee/app/models/scim_oauth_access_token.rb'
- 'ee/app/models/scoped_label_set.rb'
- - 'ee/app/models/slack_integration.rb'
- 'ee/app/models/smartcard_identity.rb'
- 'ee/app/models/software_license.rb'
- 'ee/app/models/software_license_policy.rb'
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 4fda4e8f2b6..b39f2181cee 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -1118,7 +1118,6 @@ Layout/ArgumentAlignment:
- 'ee/app/models/scim_identity.rb'
- 'ee/app/models/security/finding.rb'
- 'ee/app/models/security/orchestration_policy_rule_schedule.rb'
- - 'ee/app/models/slack_integration.rb'
- 'ee/app/models/smartcard_identity.rb'
- 'ee/app/models/status_page/project_setting.rb'
- 'ee/app/models/vulnerabilities/external_issue_link.rb'
diff --git a/.rubocop_todo/layout/space_in_lambda_literal.yml b/.rubocop_todo/layout/space_in_lambda_literal.yml
index 144b4eb04a7..7c914f47443 100644
--- a/.rubocop_todo/layout/space_in_lambda_literal.yml
+++ b/.rubocop_todo/layout/space_in_lambda_literal.yml
@@ -233,7 +233,6 @@ Layout/SpaceInLambdaLiteral:
- 'ee/app/models/security/scan.rb'
- 'ee/app/models/security/training.rb'
- 'ee/app/models/security/training_provider.rb'
- - 'ee/app/models/slack_integration.rb'
- 'ee/app/models/software_license.rb'
- 'ee/app/models/software_license_policy.rb'
- 'ee/app/models/vulnerabilities/external_issue_link.rb'
diff --git a/.rubocop_todo/lint/unused_block_argument.yml b/.rubocop_todo/lint/unused_block_argument.yml
index c09dc939ef4..263cbf9e424 100644
--- a/.rubocop_todo/lint/unused_block_argument.yml
+++ b/.rubocop_todo/lint/unused_block_argument.yml
@@ -135,7 +135,6 @@ Lint/UnusedBlockArgument:
- 'ee/spec/factories/projects.rb'
- 'ee/spec/factories/protected_branches.rb'
- 'ee/spec/factories/protected_environments.rb'
- - 'ee/spec/factories/slack_integrations.rb'
- 'ee/spec/factories/users.rb'
- 'ee/spec/features/groups/group_settings_spec.rb'
- 'ee/spec/graphql/mutations/dast/profiles/update_spec.rb'
diff --git a/.rubocop_todo/rails/inverse_of.yml b/.rubocop_todo/rails/inverse_of.yml
index de45a47fed6..412d28a11da 100644
--- a/.rubocop_todo/rails/inverse_of.yml
+++ b/.rubocop_todo/rails/inverse_of.yml
@@ -76,7 +76,6 @@ Rails/InverseOf:
- 'ee/app/models/incident_management/escalation_rule.rb'
- 'ee/app/models/incident_management/oncall_participant.rb'
- 'ee/app/models/insight.rb'
- - 'ee/app/models/integrations/gitlab_slack_application.rb'
- 'ee/app/models/iteration.rb'
- 'ee/app/models/requirements_management/requirement.rb'
- 'ee/app/models/requirements_management/test_report.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index c94541ad8a2..56d617368af 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -1035,7 +1035,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/lib/quality/seeders/vulnerabilities_spec.rb'
- 'ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb'
- 'ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb'
- - 'ee/spec/lib/slack/api_spec.rb'
- 'ee/spec/lib/slack/block_kit/app_home_opened_spec.rb'
- 'ee/spec/lib/slack/block_kit/incident_management/incident_modal_opened_spec.rb'
- 'ee/spec/lib/system_check/app/search_check_spec.rb'
diff --git a/.rubocop_todo/style/numbered_parameters.yml b/.rubocop_todo/style/numbered_parameters.yml
index 3251cc0bcad..aa3df93ab8f 100644
--- a/.rubocop_todo/style/numbered_parameters.yml
+++ b/.rubocop_todo/style/numbered_parameters.yml
@@ -25,7 +25,6 @@ Style/NumberedParameters:
- 'app/services/web_hooks/log_execution_service.rb'
- 'ee/app/models/ee/project.rb'
- 'ee/app/models/security/finding.rb'
- - 'ee/app/models/slack_integration.rb'
- 'ee/app/models/vulnerabilities/feedback.rb'
- 'ee/app/services/security/ingestion/tasks/hooks_execution.rb'
- 'ee/app/services/security/ingestion/tasks/ingest_remediations.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index b1095a278d0..8504aaee864 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-d15b9c84faee3eb178e7c7d9360832f26d4107a2
+f7fb715670e142388ce27fc6915639ec81a687b6
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 664473fccfe..48367ac42f5 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -338,9 +338,9 @@ export function renderPlayable(state, node) {
}
export function renderComment(state, node) {
- state.text('<!--');
- state.text(node.textContent);
- state.text('-->');
+ state.write('<!--');
+ state.write(node.textContent);
+ state.write('-->');
state.closeBlock(node);
}
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index 2546bface58..970ab12cc85 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -1,5 +1,10 @@
<script>
-import { GlButton, GlEmptyState, GlFilteredSearchToken, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlEmptyState,
+ GlFilteredSearchToken,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
@@ -64,7 +69,7 @@ export default {
i18n,
IssuableListTabs,
components: {
- GlButton,
+ GlDisclosureDropdown,
GlEmptyState,
IssuableList,
IssueCardStatistics,
@@ -156,6 +161,12 @@ export default {
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
+ dropdownItems() {
+ return [
+ { href: this.rssPath, text: i18n.rssLabel },
+ { href: this.calendarPath, text: i18n.calendarLabel },
+ ];
+ },
emptyStateDescription() {
return this.hasSearch ? this.$options.i18n.noSearchResultsDescription : undefined;
},
@@ -458,19 +469,14 @@ export default {
@sort="handleSort"
>
<template #nav-actions>
- <gl-button
- v-gl-tooltip
- :href="rssPath"
- icon="rss"
- :title="$options.i18n.rssLabel"
- class="has-tooltip btn-icon"
- />
- <gl-button
- v-gl-tooltip
- :href="calendarPath"
- icon="calendar"
- :title="$options.i18n.calendarLabel"
- class="has-tooltip btn-icon"
+ <gl-disclosure-dropdown
+ v-gl-tooltip="$options.i18n.actionsLabel"
+ category="tertiary"
+ icon="ellipsis_v"
+ :items="dropdownItems"
+ no-caret
+ text-sr-only
+ :toggle-text="$options.i18n.actionsLabel"
/>
</template>
diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
index 5199c36db5a..d0fca44ac15 100644
--- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
@@ -74,7 +74,7 @@ export default {
<span>
<span
v-if="issue.milestone"
- class="issuable-milestone gl-mr-3"
+ class="issuable-milestone gl-mr-3 gl-text-truncate gl-max-w-26 gl-display-inline-block gl-vertical-align-bottom"
data-testid="issuable-milestone"
>
<gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate" class="gl-font-sm">
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 2c6f11b682c..f998033b792 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -75,6 +75,7 @@ export const SPECIAL_FILTER = 'specialFilter';
export const ALTERNATIVE_FILTER = 'alternativeFilter';
export const i18n = {
+ actionsLabel: __('Actions'),
calendarLabel: __('Subscribe to calendar'),
closed: __('Closed'),
closedMoved: __('Closed (moved)'),
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 6227d5ff880..2e53324717c 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -27,9 +27,6 @@ function initNewGroupCreation(el) {
parentGroupUrl,
parentGroupName,
importExistingGroupPath,
- verificationRequired,
- verificationFormUrl,
- subscriptionsUrl,
} = el.dataset;
const props = {
@@ -48,11 +45,6 @@ function initNewGroupCreation(el) {
return new Vue({
el,
apolloProvider,
- provide: {
- verificationRequired: parseBoolean(verificationRequired),
- verificationFormUrl,
- subscriptionsUrl,
- },
render(h) {
return h(NewGroupCreationApp, { props });
},
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
index 135fa9f6228..2960298dca8 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
@@ -31,7 +31,7 @@ export default {
const { label, highlight, isLabelSet, isLabelIndeterminate } = props;
const labelColorBox = h('span', {
- class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
+ class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-absolute',
style: {
backgroundColor: label.color,
},
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
index 83df9056af2..b44096c7743 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
@@ -188,6 +188,11 @@ export default {
selectFirstItem() {
this.$refs.dropdownContentsView.selectFirstItem();
},
+ handleNewLabel(label) {
+ this.localSelectedLabels = [...this.localSelectedLabels, label];
+ this.toggleDropdownContent();
+ this.clearSearch();
+ },
},
};
</script>
@@ -229,6 +234,7 @@ export default {
:attr-workspace-path="attrWorkspacePath"
:label-create-type="labelCreateType"
@hideCreateView="toggleDropdownContent"
+ @labelCreated="handleNewLabel"
@input="clearSearch"
/>
</template>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index 30eeb0fbe31..45778640957 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -132,7 +132,7 @@ export default {
if (labelCreate.errors.length) {
[this.error] = labelCreate.errors;
} else {
- this.$emit('hideCreateView');
+ this.$emit('labelCreated', labelCreate.label);
}
} catch {
createAlert({ message: errorMessage });
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
index 314ffbaf84c..19024692bdf 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
@@ -10,9 +10,9 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-center gl-word-break-word">
+ <div class="gl-display-flex gl-align-items-center gl-word-break-word gl-relative gl-pl-4">
<span
- class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
+ class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-absolute"
:style="{ 'background-color': label.color }"
data-testid="label-color-box"
></span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index 25cf5335fb5..c19dfe663f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -5,7 +5,7 @@ import { STATUS_MERGED } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { s__, __, sprintf } from '~/locale';
+import { s__, __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
@@ -84,22 +84,13 @@ export default {
return Boolean(this.action);
},
invalidRules() {
- return this.approvals.approvalState?.rules?.filter((rule) => rule.invalid) || [];
- },
- invalidApprovedRules() {
- return this.invalidRules.filter((rule) => rule.allowMergeWhenInvalid);
- },
- invalidFailedRules() {
- return this.invalidRules.filter((rule) => !rule.allowMergeWhenInvalid);
+ return this.approvals.approvalState?.invalidApproversRules || [];
},
hasInvalidRules() {
return this.mr.mergeRequestApproversAvailable && this.invalidRules.length;
},
- hasInvalidApprovedRules() {
- return this.mr.mergeRequestApproversAvailable && this.invalidApprovedRules.length;
- },
- hasInvalidFailedRules() {
- return this.mr.mergeRequestApproversAvailable && this.invalidFailedRules.length;
+ invalidRulesText() {
+ return this.invalidRules.length;
},
approvedBy() {
return this.approvals.approvedBy?.nodes || [];
@@ -142,29 +133,11 @@ export default {
return null;
},
- pluralizedApprovedRuleText() {
- return this.invalidApprovedRules.length > 1
+ pluralizedRuleText() {
+ return this.invalidRules.length > 1
? this.$options.i18n.invalidRulesPlural
: this.$options.i18n.invalidRuleSingular;
},
- pluralizedFailedRuleText() {
- return this.invalidFailedRules.length > 1
- ? this.$options.i18n.invalidFailedRulesPlural
- : this.$options.i18n.invalidFailedRuleSingular;
- },
- pluralizedRuleText() {
- return [
- this.hasInvalidFailedRules
- ? sprintf(this.pluralizedFailedRuleText, { rules: this.invalidFailedRules.length })
- : null,
- this.hasInvalidApprovedRules
- ? sprintf(this.pluralizedApprovedRuleText, { rules: this.invalidApprovedRules.length })
- : null,
- ]
- .filter((text) => Boolean(text))
- .join(', ')
- .concat('.');
- },
},
methods: {
approve() {
@@ -232,13 +205,11 @@ export default {
FETCH_LOADING,
linkToInvalidRules: INVALID_RULES_DOCS_PATH,
i18n: {
- invalidRuleSingular: s__('mrWidget|%{rules} invalid rule has been approved automatically'),
- invalidRulesPlural: s__('mrWidget|%{rules} invalid rules have been approved automatically'),
- invalidFailedRuleSingular: s__(
- "mrWidget|%{dangerStart}%{rules} rule can't be approved%{dangerEnd}",
+ invalidRuleSingular: s__(
+ 'mrWidget|%{rules} invalid rule has been approved automatically, as no one can approve it.',
),
- invalidFailedRulesPlural: s__(
- "mrWidget|%{dangerStart}%{rules} rules can't be approved%{dangerEnd}",
+ invalidRulesPlural: s__(
+ 'mrWidget|%{rules} invalid rules have been approved automatically, as no one can approve them.',
),
learnMore: __('Learn more.'),
},
@@ -284,9 +255,7 @@ export default {
</div>
<div v-if="hasInvalidRules" class="gl-text-gray-400 gl-mt-2" data-testid="invalid-rules">
<gl-sprintf :message="pluralizedRuleText">
- <template #danger="{ content }">
- <span class="gl-font-weight-bold text-danger">{{ content }}</span>
- </template>
+ <template #rules>{{ invalidRulesText }}</template>
</gl-sprintf>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index a68c577bff6..0798d76080f 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -104,10 +104,12 @@ export default {
return sprintf(__('closed %{timeago}'), {
timeago: getTimeago().format(this.issuable.closedAt),
});
+ } else if (this.issuable.updatedAt !== this.issuable.createdAt) {
+ return sprintf(__('updated %{timeAgo}'), {
+ timeAgo: getTimeago().format(this.issuable.updatedAt),
+ });
}
- return sprintf(__('updated %{timeAgo}'), {
- timeAgo: getTimeago().format(this.issuable.updatedAt),
- });
+ return undefined;
},
issuableTitleProps() {
if (this.isIssuableUrlExternal) {
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 31fd9e0a0ec..21ffd0fc23b 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -13,17 +13,10 @@ export default {
GlIcon,
WelcomePage,
LegacyContainer,
- CreditCardVerification: () =>
- import('ee_component/namespaces/verification/components/credit_card_verification.vue'),
},
directives: {
SafeHtml,
},
- inject: {
- verificationRequired: {
- default: false,
- },
- },
props: {
title: {
type: String,
@@ -51,7 +44,6 @@ export default {
data() {
return {
activePanelName: null,
- verificationCompleted: false,
};
},
@@ -84,10 +76,6 @@ export default {
: this.initialBreadcrumbs;
},
- shouldVerify() {
- return this.verificationRequired && !this.verificationCompleted;
- },
-
showNewTopLevelGroupAlert() {
if (this.activePanel.detailProps === undefined) {
return false;
@@ -121,16 +109,12 @@ export default {
localStorage.setItem(this.persistenceKey, this.activePanelName);
}
},
- onVerified() {
- this.verificationCompleted = true;
- },
},
};
</script>
<template>
- <credit-card-verification v-if="shouldVerify" @verified="onVerified" />
- <div v-else-if="!activePanelName" class="gl-mt-4">
+ <div v-if="!activePanelName" class="gl-mt-4">
<gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
<welcome-page :panels="panels" :title="title">
<template #footer>
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 5fa7fbdef99..7cb1fc293bd 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -718,12 +718,10 @@
}
.dropdown-label-box {
- position: relative;
top: 0;
- margin-right: 5px;
- display: inline-block;
- width: 15px;
- height: 15px;
+ left: 0;
+ height: 100%;
+ width: $gl-spacing-scale-2;
border-radius: $border-radius-base;
}
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index b101f184ca6..6d6138057cf 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module NavHelper
+ extend self
+
def header_links
@header_links ||= get_header_links
end
@@ -84,22 +86,17 @@ module NavHelper
%w(dev_ops_report usage_trends)
end
- def show_super_sidebar?
- Feature.enabled?(:super_sidebar_nav, current_user) && current_user&.use_new_navigation && super_sidebar_supported?
+ def show_super_sidebar?(user = current_user)
+ # The new sidebar is not enabled for anonymous use
+ # Once we enable the new sidebar by default, this
+ # should return true
+ return false unless user
+
+ Feature.enabled?(:super_sidebar_nav, user) && user.use_new_navigation
end
private
- # This is a temporary measure until we support all other existing sidebars:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/391500
- # https://gitlab.com/gitlab-org/gitlab/-/issues/391501
- # https://gitlab.com/gitlab-org/gitlab/-/issues/391502
- def super_sidebar_supported?
- return true if @nav.nil?
-
- %w(your_work explore project group profile user_profile search admin).include?(@nav)
- end
-
def get_header_links
links = if current_user
[:user_dropdown]
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
index 9efc85cbdb1..5e502cce927 100644
--- a/app/models/integrations/apple_app_store.rb
+++ b/app/models/integrations/apple_app_store.rb
@@ -6,6 +6,7 @@ module Integrations
class AppleAppStore < Integration
ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze
KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze
+ IS_KEY_CONTENT_BASE64 = "true"
SECTION_TYPE_APPLE_APP_STORE = 'apple_app_store'
@@ -43,7 +44,8 @@ module Integrations
variable_list = [
'<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>',
'<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>',
- '<code>APP_STORE_CONNECT_API_KEY_KEY</code>'
+ '<code>APP_STORE_CONNECT_API_KEY_KEY</code>',
+ '<code>APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64</code>'
]
# rubocop:disable Layout/LineLength
@@ -92,7 +94,9 @@ module Integrations
{ key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: app_store_issuer_id, masked: true, public: false },
{ key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(app_store_private_key), masked: true,
public: false },
- { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false }
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64', value: IS_KEY_CONTENT_BASE64, masked: false,
+ public: false }
]
end
diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb
new file mode 100644
index 00000000000..b0f54f39e8c
--- /dev/null
+++ b/app/models/integrations/gitlab_slack_application.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+module Integrations
+ class GitlabSlackApplication < BaseSlackNotification
+ attribute :alert_events, default: false
+ attribute :commit_events, default: false
+ attribute :confidential_issues_events, default: false
+ attribute :confidential_note_events, default: false
+ attribute :deployment_events, default: false
+ attribute :issues_events, default: false
+ attribute :job_events, default: false
+ attribute :merge_requests_events, default: false
+ attribute :note_events, default: false
+ attribute :pipeline_events, default: false
+ attribute :push_events, default: false
+ attribute :tag_push_events, default: false
+ attribute :vulnerability_events, default: false
+ attribute :wiki_page_events, default: false
+
+ has_one :slack_integration, foreign_key: :integration_id, inverse_of: :integration
+ delegate :bot_access_token, :bot_user_id, to: :slack_integration, allow_nil: true
+
+ def update_active_status
+ update(active: !!slack_integration)
+ end
+
+ def title
+ s_('Integrations|GitLab for Slack app')
+ end
+
+ def description
+ s_('Integrations|Enable slash commands and notifications for a Slack workspace.')
+ end
+
+ def self.to_param
+ 'gitlab_slack_application'
+ end
+
+ override :show_active_box?
+ def show_active_box?
+ false
+ end
+
+ override :test
+ def test(_data)
+ failures = test_notification_channels
+
+ { success: failures.blank?, result: failures }
+ end
+
+ # The form fields of this integration are editable only after the Slack App installation
+ # flow has been completed, which causes the integration to become activated/enabled.
+ override :editable?
+ def editable?
+ activated?
+ end
+
+ override :fields
+ def fields
+ return [] unless editable?
+
+ super
+ end
+
+ override :sections
+ def sections
+ return [] unless editable?
+
+ [
+ {
+ type: SECTION_TYPE_TRIGGER,
+ title: s_('Integrations|Trigger'),
+ description: s_('Integrations|An event will be triggered when one of the following items happen.')
+ },
+ {
+ type: SECTION_TYPE_CONFIGURATION,
+ title: s_('Integrations|Notification settings'),
+ description: s_('Integrations|Configure the scope of notifications.')
+ }
+ ]
+ end
+
+ override :configurable_events
+ def configurable_events
+ return [] unless editable?
+
+ super
+ end
+
+ override :requires_webhook?
+ def requires_webhook?
+ false
+ end
+
+ def upgrade_needed?
+ slack_integration.present? && slack_integration.upgrade_needed?
+ end
+
+ private
+
+ override :notify
+ def notify(message, opts)
+ channels = Array(opts[:channel])
+ return false if channels.empty?
+
+ payload = {
+ attachments: message.attachments,
+ text: message.pretext,
+ unfurl_links: false,
+ unfurl_media: false
+ }
+
+ successes = channels.map do |channel|
+ notify_slack_channel!(channel, payload)
+ end
+
+ successes.any?
+ end
+
+ def notify_slack_channel!(channel, payload)
+ response = api_client.post(
+ 'chat.postMessage',
+ payload.merge(channel: channel)
+ )
+
+ log_error('Slack API error when notifying', api_response: response.parsed_response) unless response['ok']
+
+ response['ok']
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
+ {
+ integration_id: id,
+ slack_integration_id: slack_integration.id
+ }
+ )
+
+ false
+ end
+
+ def api_client
+ @slack_api ||= ::Slack::API.new(slack_integration)
+ end
+
+ def test_notification_channels
+ return if unique_channels.empty?
+ return s_('Integrations|GitLab for Slack app must be reinstalled to enable notifications') unless bot_access_token
+
+ test_payload = {
+ text: 'Test',
+ user: bot_user_id
+ }
+
+ not_found_channels = unique_channels.first(10).select do |channel|
+ test_payload[:channel] = channel
+
+ response = ::Slack::API.new(slack_integration).post('chat.postEphemeral', test_payload)
+ response['error'] == 'channel_not_found'
+ end
+
+ return if not_found_channels.empty?
+
+ format(
+ s_(
+ 'Integrations|Unable to post to %{channel_list}, ' \
+ 'please add the GitLab Slack app to any private Slack channels'
+ ),
+ channel_list: not_found_channels.to_sentence
+ )
+ end
+
+ override :metrics_key_prefix
+ def metrics_key_prefix
+ 'i_integrations_gitlab_for_slack_app'
+ end
+ end
+end
diff --git a/app/models/integrations/slack_workspace/api_scope.rb b/app/models/integrations/slack_workspace/api_scope.rb
new file mode 100644
index 00000000000..3c4d25bff10
--- /dev/null
+++ b/app/models/integrations/slack_workspace/api_scope.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackWorkspace
+ class ApiScope < ApplicationRecord
+ self.table_name = 'slack_api_scopes'
+
+ def self.find_or_initialize_by_names(names)
+ found = where(name: names).to_a
+ missing_names = names - found.pluck(:name)
+
+ if missing_names.any?
+ insert_all(missing_names.map { |name| { name: name } })
+ missing = where(name: missing_names)
+ found += missing
+ end
+
+ found
+ end
+ end
+ end
+end
diff --git a/app/models/integrations/slack_workspace/integration_api_scope.rb b/app/models/integrations/slack_workspace/integration_api_scope.rb
new file mode 100644
index 00000000000..d33c8e0d816
--- /dev/null
+++ b/app/models/integrations/slack_workspace/integration_api_scope.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackWorkspace
+ class IntegrationApiScope < ApplicationRecord
+ self.table_name = 'slack_integrations_scopes'
+
+ belongs_to :slack_api_scope, class_name: 'Integrations::SlackWorkspace::ApiScope'
+ belongs_to :slack_integration
+
+ # Efficient scope propagation
+ def self.update_scopes(integration_ids, scopes)
+ return if integration_ids.empty?
+
+ scope_ids = scopes.pluck(:id)
+
+ attrs = scope_ids.flat_map do |scope_id|
+ integration_ids.map { |si_id| { slack_integration_id: si_id, slack_api_scope_id: scope_id } }
+ end
+
+ # We don't know which ones to preserve - so just delete them all in a single query
+ transaction do
+ where(slack_integration_id: integration_ids).delete_all
+ insert_all(attrs) unless attrs.empty?
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 1e7ff6e8f0e..2d4c4daafe8 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -38,7 +38,10 @@ class MergeRequest < ApplicationRecord
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
'Ci::CompareMetricsReportsService' => ->(project) { true },
- 'Ci::CompareCodequalityReportsService' => ->(project) { true }
+ 'Ci::CompareCodequalityReportsService' => ->(project) { true },
+ 'Ci::CompareSecurityReportsService' => ->(project) do
+ Feature.enabled?(:use_merge_base_for_security_widget, project)
+ end
}.freeze
belongs_to :target_project, class_name: "Project"
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index 61129bbc9d8..d305a4ace51 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -4,9 +4,6 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent
belongs_to :milestone
scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
- scope :aliased_for_timebox_report, -> do
- select("'timebox' AS event_type", "id", "created_at", "milestone_id AS value", "action", "issue_id")
- end
# state is used for issue and merge request states.
enum state: Issue.available_states.merge(MergeRequest.available_states)
@@ -23,3 +20,5 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent
MilestoneNote
end
end
+
+ResourceMilestoneEvent.prepend_mod
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index e2ac762b1cd..134f71e35ad 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -13,10 +13,6 @@ class ResourceStateEvent < ResourceEvent
after_create :issue_usage_metrics
- scope :aliased_for_timebox_report, -> do
- select("'state' AS event_type", "id", "created_at", "state AS value", "NULL AS action", "issue_id")
- end
-
def self.issuable_attrs
%i(issue merge_request).freeze
end
diff --git a/app/models/slack_integration.rb b/app/models/slack_integration.rb
new file mode 100644
index 00000000000..22e911aeacd
--- /dev/null
+++ b/app/models/slack_integration.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+class SlackIntegration < ApplicationRecord
+ include EachBatch
+
+ ALL_FEATURES = %i[commands notifications].freeze
+
+ SCOPE_COMMANDS = 'commands'
+ SCOPE_CHAT_WRITE = 'chat:write'
+ SCOPE_CHAT_WRITE_PUBLIC = 'chat:write.public'
+
+ # These scopes are requested when installing the app, additional scopes
+ # will need reauthorization.
+ # https://api.slack.com/authentication/oauth-v2#asking
+ SCOPES = [SCOPE_COMMANDS, SCOPE_CHAT_WRITE, SCOPE_CHAT_WRITE_PUBLIC].freeze
+
+ belongs_to :integration
+
+ attr_encrypted :bot_access_token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm',
+ encode: false,
+ encode_iv: false
+
+ has_many :slack_integrations_scopes,
+ class_name: '::Integrations::SlackWorkspace::IntegrationApiScope'
+
+ has_many :slack_api_scopes,
+ class_name: '::Integrations::SlackWorkspace::ApiScope',
+ through: :slack_integrations_scopes
+
+ scope :with_bot, -> { where.not(bot_user_id: nil) }
+ scope :by_team, ->(team_id) { where(team_id: team_id) }
+
+ validates :team_id, presence: true
+ validates :team_name, presence: true
+ validates :alias, presence: true,
+ uniqueness: { scope: :team_id, message: 'This alias has already been taken' },
+ length: 2..4096
+ validates :user_id, presence: true
+ validates :integration, presence: true
+
+ after_commit :update_active_status_of_integration, on: [:create, :destroy]
+
+ def update_active_status_of_integration
+ integration.update_active_status
+ end
+
+ def feature_available?(feature_name)
+ case feature_name
+ when :commands
+ # The slash commands feature requires 'commands' scope.
+ # All records will support this scope, as this was the original feature.
+ true
+ when :notifications
+ scoped_to?(SCOPE_CHAT_WRITE, SCOPE_CHAT_WRITE_PUBLIC)
+ else
+ false
+ end
+ end
+
+ def upgrade_needed?
+ !all_features_supported?
+ end
+
+ def all_features_supported?
+ ALL_FEATURES.all? { |feature| feature_available?(feature) } # rubocop: disable Gitlab/FeatureAvailableUsage
+ end
+
+ def authorized_scope_names=(names)
+ names = Array.wrap(names).flat_map { |name| name.split(',') }.map(&:strip)
+
+ scopes = ::Integrations::SlackWorkspace::ApiScope.find_or_initialize_by_names(names)
+ self.slack_api_scopes = scopes
+ end
+
+ def authorized_scope_names
+ slack_api_scopes.pluck(:name)
+ end
+
+ private
+
+ def scoped_to?(*names)
+ return false if names.empty?
+
+ names.to_set <= all_scopes
+ end
+
+ def all_scopes
+ @all_scopes = authorized_scope_names.to_set
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 96223ac5027..0aa509e58d7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -380,7 +380,6 @@ class User < ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
- delegate :requires_credit_card_verification, :requires_credit_card_verification=, to: :user_detail, allow_nil: true
delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true
delegate :twitter, :twitter=, to: :user_detail, allow_nil: true
delegate :skype, :skype=, to: :user_detail, allow_nil: true
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 9d3df3d6400..293a20fcc5a 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
class UserDetail < ApplicationRecord
+ include IgnorableColumns
extend ::Gitlab::Utils::Override
+ ignore_column :requires_credit_card_verification, remove_with: '16.1', remove_after: '2023-06-22'
+
REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze
belongs_to :user
diff --git a/app/models/work_items/resource_link_event.rb b/app/models/work_items/resource_link_event.rb
index 64d51b2743c..6725acf8c68 100644
--- a/app/models/work_items/resource_link_event.rb
+++ b/app/models/work_items/resource_link_event.rb
@@ -12,3 +12,5 @@ module WorkItems
}
end
end
+
+WorkItems::ResourceLinkEvent.prepend_mod
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 7190c82bea3..61cf598f178 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -2,9 +2,8 @@
module Users
class UpsertCreditCardValidationService < BaseService
- def initialize(params, user)
+ def initialize(params)
@params = params.to_h.with_indifferent_access
- @current_user = user
end
def execute
@@ -19,8 +18,6 @@ module Users
::Users::CreditCardValidation.upsert(@params)
- ::Users::UpdateService.new(current_user, user: current_user, requires_credit_card_verification: false).execute!
-
ServiceResponse.success(message: 'CreditCardValidation was set')
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index af27026845e..b9042d3a36b 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -48,7 +48,7 @@
- if Feature.enabled?(:super_sidebar_nav, current_user)
%li.divider
- .js-new-nav-toggle{ data: { enabled: current_user.use_new_navigation.to_s, endpoint: profile_preferences_url} }
+ .js-new-nav-toggle{ data: { enabled: show_super_sidebar?.to_s, endpoint: profile_preferences_url} }
- if current_user_menu?(:sign_out)
%li.divider
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 85396134db2..dd87056af40 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -27,13 +27,13 @@
#{s_('IssueList|created %{timeAgoString} by %{user}').html_safe % { timeAgoString: time_ago_with_tooltip(merge_request.created_at, placement: 'bottom'), user: link_to_member(@project, merge_request.author, avatar: false) }}
= render_if_exists 'shared/issuable/gitlab_team_member_badge', author: merge_request.author
- if merge_request.milestone
- %span.issuable-milestone.d-none.d-sm-inline-block
+ %span.issuable-milestone.d-none.d-sm-inline-block.gl-text-truncate.gl-max-w-26.gl-vertical-align-bottom
&nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
- %span.project-ref-path.has-tooltip{ title: _('Target branch') }
+ %span.project-ref-path.has-tooltip.d-inline-block.gl-text-truncate.gl-max-w-26.gl-vertical-align-bottom{ title: _('Target branch: %{target_branch}') % {target_branch: merge_request.target_branch} }
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
= sprite_icon('branch', size: 12, css_class: 'fork-sprite')
@@ -66,6 +66,7 @@
= render 'shared/issuable_meta_data', issuable: merge_request
- .float-right.issuable-updated-at.d-none.d-sm-inline-block.gl-text-gray-500
- %span
- = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago') }
+ - if merge_request.updated_at != merge_request.created_at
+ .float-right.issuable-updated-at.d-none.d-sm-inline-block.gl-text-gray-500
+ %span
+ = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago') }
diff --git a/config/feature_flags/development/use_merge_base_for_security_widget.yml b/config/feature_flags/development/use_merge_base_for_security_widget.yml
new file mode 100644
index 00000000000..434e6086bcb
--- /dev/null
+++ b/config/feature_flags/development/use_merge_base_for_security_widget.yml
@@ -0,0 +1,8 @@
+---
+name: use_merge_base_for_security_widget
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117594
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/407762
+milestone: '16.0'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/doc/api/users.md b/doc/api/users.md
index c17dfd12c8b..5706bed0243 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -838,7 +838,8 @@ Example response:
"id": 1,
"user_id": 1
"view_diffs_file_by_file": true,
- "show_whitespace_in_diffs": false
+ "show_whitespace_in_diffs": false,
+ "pass_user_identities_to_ci_jwt": false
}
```
@@ -859,16 +860,18 @@ PUT /user/preferences
"id": 1,
"user_id": 1
"view_diffs_file_by_file": true,
- "show_whitespace_in_diffs": false
+ "show_whitespace_in_diffs": false,
+ "pass_user_identities_to_ci_jwt": false
}
```
Parameters:
-| Attribute | Required | Description |
-| :--------------------------- | :------- | :---------------------------------------------------------- |
-| `view_diffs_file_by_file` | Yes | Flag indicating the user sees only one file diff per page. |
-| `show_whitespace_in_diffs` | Yes | Flag indicating the user sees whitespace changes in diffs. |
+| Attribute | Required | Description |
+| :------------------------------- | :------- | :--------------------------------------------------------------------------- |
+| `view_diffs_file_by_file` | Yes | Flag indicating the user sees only one file diff per page. |
+| `show_whitespace_in_diffs` | Yes | Flag indicating the user sees whitespace changes in diffs. |
+| `pass_user_identities_to_ci_jwt` | Yes | Flag indicating the user passes their external identities as CI information. |
## User follow
diff --git a/doc/architecture/blueprints/cells/glossary.md b/doc/architecture/blueprints/cells/glossary.md
new file mode 100644
index 00000000000..132a9656c6d
--- /dev/null
+++ b/doc/architecture/blueprints/cells/glossary.md
@@ -0,0 +1,106 @@
+---
+stage: enablement
+group: Tenant Scale
+description: 'Cells: Glossary'
+---
+
+# Cells: Glossary
+
+We use the following terms to describe components and properties of the Cells architecture.
+
+## Cell
+
+> Pod was renamed to Cell in <https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/121163>
+
+A Cell is a set of infrastructure components that contains multiple top-level namespaces that belong to different organizations. The components include both datastores (PostgreSQL, Redis etc.) and stateless services (web etc.). The infrastructure components provided within a Cell are shared among organizations and their top-level namespaces but not shared with other Cells. This isolation of infrastructure components means that Cells are independent from each other.
+
+<img src="images/term-cell.png" height="200">
+
+### Cell properties
+
+- Each cell is independent from the others
+- Infrastructure components are shared by organizations and their top-level namespaces within a Cell
+- More Cells can be provisioned to provide horizontal scalability
+- A failing Cell does not lead to failure of other Cells
+- Noisy neighbor effects are limited to a Cell
+- Cells are not visible to organizations; it is an implementation detail
+- Cells may be located in different geographical regions (for example, EU, US, JP, UK)
+
+Discouraged synonyms: GitLab instance, cluster, shard
+
+## Cluster
+
+A cluster is a collection of Cells.
+
+<img src="images/term-cluster.png" height="300">
+
+### Cluster properties
+
+- A cluster holds cluster-wide metadata, for example Users, Routes, Settings.
+
+Discouraged synonyms: whale
+
+## Organizations
+
+GitLab references [Organizations in the initial set up](../../../topics/set_up_organization.md) and users can add a (free text) organization to their profile. There is no Organization entity established in the GitLab codebase.
+
+As part of delivering Cells, we propose the introduction of an `organization` entity. Organizations would represent billable entities or customers.
+
+Organizations are a known concept, present for example in [AWS](https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/core-concepts.html) and [GCP](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#organizations).
+
+Organizations work under the following assumptions:
+
+1. Users care about what happens within their organizations.
+1. Features need to work within an organization.
+1. Only few features need to work across organizations.
+1. Users understand that the majority of pages they view are only scoped to a single organization at a time.
+1. Organizations are located on a single cell.
+
+![Term Organization](images/term-organization.png)
+
+### Organization properties
+
+- Top-level namespaces belong to organizations
+- Organizations are isolated from each other by default meaning that cross-namespace features will only work for namespaces that exist within a single organization
+- User namespaces must not belong to an organization
+
+Discouraged synonyms: Billable entities, customers
+
+## Top-Level namespace
+
+Top-level namespace is the name given to the top most group of all other groups. Groups and projects are nested underneath the top-level namespace.
+
+Example:
+
+`https://gitlab.com/gitlab-org/gitlab/`:
+
+- `gitlab-org` is a `top-level namespace`; the root for all groups and projects of an organization
+- `gitlab` is a `project`; a project of the organization.
+
+The top-level namespace has served as the defacto Organization entity. With the creation of Organization, top-level namespaces will be [nested underneath Organizations](https://gitlab.com/gitlab-org/gitlab/-/issues/394796).
+
+Over time there won't be a distinction between a top level namespace and a group. All features that make Top-level namespaces different from groups will move to Organization.
+
+Discouraged synonyms: Root-level namespace
+
+![Term Top-level Namespace](images/term-top-level-namespace.png)
+
+### Top-level namespace properties
+
+- Top-level namespaces belonging to an organization are located on the same Cell
+- Top-level namespaces can interact with other top-level namespaces that belong to the same organization
+
+## Users
+
+Users are available globally and not restricted to a single Cell. Users belong to a single organization, but can participate in many organizations through group and project membership with varying permissions. Inside organizations, users can create multiple top-level namespaces. User activity is not limited to a single organization but their contributions (for example TODOs) are only aggregated within an organization. This avoids the need for aggregating across cells.
+
+### User properties
+
+- Users are shared globally across all Cells
+- Users can create multiple top-level namespaces
+- Users can be a member of multiple top-level namespaces
+- Users belong to one organization. See [!395736](https://gitlab.com/gitlab-org/gitlab/-/issues/395736)
+- Users can be members of groups and projects in different organizations
+- Users can administer organizations
+- User activity is aggregated in an organization
+- Every user has one personal namespace
diff --git a/doc/architecture/blueprints/cells/goals.md b/doc/architecture/blueprints/cells/goals.md
new file mode 100644
index 00000000000..a6bab4a6748
--- /dev/null
+++ b/doc/architecture/blueprints/cells/goals.md
@@ -0,0 +1,59 @@
+---
+stage: enablement
+group: Tenant Scale
+description: 'Cells: Goals'
+---
+
+# Cells: Goals
+
+## Scalability
+
+The main goal of this new shared-infrastructure architecture is to provide additional scalability for our SaaS Platform. GitLab.com is largely monolithic and we have estimated (internal) that the current architecture has scalability limitations, even when database partitioning and decomposition are taken into account.
+
+Cells provide a horizontally scalable solution because additional Cells can be created based on demand. Cells can be provisioned and tuned as needed for optimal scalability.
+
+## Increased availability
+
+A major challenge for shared-infrastructure architectures is a lack of isolation between top-level namespaces. This can lead to noisy neighbor effects. A organization's behavior inside a top-level namespace can impact all other organizations. This is highly undesirable. Cells provide isolation at the cell level. A group of organizations is fully isolated from other organizations located on a different Cell. This minimizes noisy neighbor effects while still benefiting from the cost-efficiency of shared infrastructure.
+
+Additionally, Cells provide a way to implement disaster recovery capabilities. Entire Cells may be replicated to read-only standbys with automatic failover capabilities.
+
+## A consistent experience
+
+Organizations should have the same user experience on our SaaS platform as they do on a self-managed GitLab instance.
+
+## Regions
+
+GitLab.com is only hosted within the United States of America. Organizations located in other regions have voiced demand for local SaaS offerings. Cells provide a path towards [GitLab Regions](https://gitlab.com/groups/gitlab-org/-/epics/6037) because Cells may be deployed within different geographies. Depending on which of the organization's data is located outside a Cell, this may solve data residency and compliance problems.
+
+## Market segment
+
+Cells would provide a solution for organizations in the small to medium business (up to 100 users) and the mid-market segment (up to 2000 users).
+(See [segmentation definitions](https://about.gitlab.com/handbook/sales/field-operations/gtm-resources/#segmentation).)
+Larger organizations may benefit substantially from [GitLab Dedicated](../../../subscriptions/gitlab_dedicated/index.md).
+
+At this moment, GitLab.com has "social-network"-like capabilities that may not fit well into a more isolated organization model. Removing those features, however, possesses some challenges:
+
+1. How will existing `gitlab-org` contributors contribute to the namespace??
+1. How do we move existing top-level namespaces into the new model (effectively breaking their social features)?
+
+We should evaluate if the SMB and mid market segment is interested in these features, or if not having them is acceptable in most cases.
+
+## Self-managed
+
+For reasons of consistency, it is expected that self-managed instances will
+adopt the cells architecture as well. To expand, self-managed instances can
+continue with just a single Cell while supporting the option of adding additional
+Cells. Organizations, and possible User decomposition will also be adopted for
+self-managed instances.
+
+## High-level architecture problems to solve
+
+A number of technical issues need to be resolved to implement Cells (in no particular order). This section will be expanded.
+
+1. How are Cells provisioned? - [Design discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/396641)
+1. What is a Cells topology? - [Design discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/396641)
+1. How are users of an organization routed to the correct Cell? -
+1. How do users authenticate with Cells and Organizations? - [Design discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/395736)
+1. How are Cells rebalanced?
+1. How can Cells implement disaster recovery capabilities?
diff --git a/doc/architecture/blueprints/cells/impact.md b/doc/architecture/blueprints/cells/impact.md
new file mode 100644
index 00000000000..402af485b09
--- /dev/null
+++ b/doc/architecture/blueprints/cells/impact.md
@@ -0,0 +1,58 @@
+---
+stage: enablement
+group: Tenant Scale
+description: 'Cells: Cross-section impact'
+---
+
+# Cells: Cross-section impact
+
+Cells is a fundamental architecture change that impacts other sections and stages. This section summarizes and links to other groups that may be impacted and highlights potential conflicts that need to be resolved. The Tenant Scale group is not responsible for achieving the goals of other groups but we want to ensure that dependencies are resolved.
+
+## Summary
+
+Based on discussions with other groups the net impact of introducing Cells and a new entity called organizations is mostly neutral. It may slow down development in some areas. We did not discover major blockers for other teams.
+
+1. We need to resolve naming conflicts (proposal is TBD)
+1. Cells requires introducing Organizations. Organizations are a new entity **above** top-level groups. Because this is a new entity, it may impact the ability to consolidate settings for Group::Organization and influence their decision on [how to approach introducing a an organization](https://gitlab.com/gitlab-org/gitlab/-/issues/376285#approach-2-organization-is-built-on-top-of-top-level-groups)
+1. Organizations may make it slightly easier for Fulfillment to realize their billing plans.
+
+## Impact on Group::Organization
+
+We synced with the Organization PM and Designer ([recording](https://youtu.be/b5Opn9cFWFk)) and discussed the similarities and differences between the Cells and Organization proposal ([presentation](https://docs.google.com/presentation/d/1FsUi22Up15b_tu6p2m-yLML3hCZ3rgrZrmzJAxUsNmU/edit?usp=sharing)).
+
+### Goals of Group::Organization
+
+As defined in the [organization documentation](../../../user/organization/index.md):
+
+1. Create an entity to manage everything you do as a GitLab administrator, including:
+ 1. Defining and applying settings to all of your groups, subgroups, and projects.
+ 1. Aggregating data from all your groups, subgroups, and projects.
+1. Reach feature parity between SaaS and self-managed installations, with all Admin Area settings moving to groups (?). Hardware controls remain on the instance level.
+
+The [organization roadmap outlines](https://gitlab.com/gitlab-org/gitlab/-/issues/368237#high-level-goals) the current goals in detail.
+
+### Potential conflicts with Cells
+
+- Organization defines a new entity as the primary organizational object for groups and projects.
+- We will only introduce one entity
+- Group::Organization highlighted the need to further validate the key assumption that users only care about what happens within their organization.
+
+## Impact on Fulfillment
+
+We synced with Fulfillment ([recording](https://youtu.be/FkQF3uF7vTY)) to discuss how Cells would impact them. Fulfillment is supportive of an entity above top-level namespaces. Their perspective is outline in [!5639](https://gitlab.com/gitlab-org/customers-gitlab-com/-/merge_requests/5639/diffs).
+
+### Goals of Fulfillment
+
+- Fulfillment has a longstanding plan to move billing from the top-level namespace to a level above. This would mean that a license applies for an organization and all its top-level namespaces.
+- Fulfillment uses Zuora for billing and would like to have a 1-to-1 relationship between an organization and their Zuora entity called BillingAccount. They want to move away from tying a license to a single user.
+- If a customer needs multiple organizations, the corresponding BillingAccounts can be rolled up into a consolidated billing account (similar to [AWS consolidated billing](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/consolidated-billing.html))
+- Ideally, a self-managed instance has a single Organization by default, which should be enough for most customers.
+- Fulfillment prefers only one additional entity.
+
+A rough representation of this is:
+
+![Cells and Fulfillment](images/pods-and-fulfillment.png)
+
+### Potential conflicts with Cells
+
+- There are no known conflicts between Fulfillment's plans and Cells
diff --git a/doc/architecture/blueprints/cells/index.md b/doc/architecture/blueprints/cells/index.md
index 3b88e2f731c..f415847bbb4 100644
--- a/doc/architecture/blueprints/cells/index.md
+++ b/doc/architecture/blueprints/cells/index.md
@@ -14,221 +14,13 @@ participating-stages: []
This document is a work-in-progress and represents a very early state of the Cells design. Significant aspects are not documented, though we expect to add them in the future.
-## Summary
+Cells is a new architecture for our Software as a Service platform. This architecture is horizontally-scalable, resilient, and provides a more consistent user experience. It may also provide additional features in the future, such as data residency control (regions) and federated features.
-Cells is a new architecture for our Software as a Service platform that is horizontally-scalable, resilient, and provides a more consistent user experience. It may also provide additional features in the future, such as data residency control (regions) and federated features.
+For more information about Cells, see also:
-## Terminology
-
-We use the following terms to describe components and properties of the Cells architecture.
-
-### Cell
-
-> Pod was renamed to Cell in <https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/121163>
-
-A Cell is a set of infrastructure components that contains multiple top-level namespaces that belong to different organizations. The components include both datastores (PostgreSQL, Redis etc.) and stateless services (web etc.). The infrastructure components provided within a Cell are shared among organizations and their top-level namespaces but not shared with other Cells. This isolation of infrastructure components means that Cells are independent from each other.
-
-![Term Cell](images/term-cell.png)
-
-#### Cell properties
-
-- Each cell is independent from the others
-- Infrastructure components are shared by organizations and their top-level namespaces within a Cell
-- More Cells can be provisioned to provide horizontal scalability
-- A failing Cell does not lead to failure of other Cells
-- Noisy neighbor effects are limited to within a Cell
-- Cells are not visible to organizations; it is an implementation detail
-- Cells may be located in different geographical regions (for example, EU, US, JP, UK)
-
-Discouraged synonyms: GitLab instance, cluster, shard
-
-### Cluster
-
-A cluster is a collection of Cells.
-
-![Term Cluster](images/term-cluster.png)
-
-#### Cluster properties
-
-- A cluster holds cluster-wide metadata, for example Users, Routes, Settings.
-
-Discouraged synonyms: whale
-
-### Organizations
-
-GitLab references [Organizations in the initial set up](../../../topics/set_up_organization.md) and users can add a (free text) organization to their profile. There is no Organization entity established in the GitLab codebase.
-
-As part of delivering Cells, we propose the introduction of an `organization` entity. Organizations would represent billable entities or customers.
-
-Organizations are a known concept, present for example in [AWS](https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/core-concepts.html) and [GCP](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#organizations).
-
-Organizations work under the following assumptions:
-
-1. Users care about what happens within their organizations.
-1. Features need to work within an organization.
-1. Only few features need to work across organizations.
-1. Users understand that the majority of pages they view are only scoped to a single organization at a time.
-1. Organizations are located on a single cell.
-
-![Term Organization](images/term-organization.png)
-
-#### Organization properties
-
-- Top-level namespaces belong to organizations
-- Organizations are isolated from each other by default meaning that cross-namespace features will only work for namespaces that exist within a single organization
-- User namespaces must not belong to an organization
-
-Discouraged synonyms: Billable entities, customers
-
-### Top-Level namespace
-
-Top-level namespace is the name given to the top most group of all other groups. Groups and projects are nested underneath the top-level namespace.
-
-Example:
-
-`https://gitlab.com/gitlab-org/gitlab/`:
-
-- `gitlab-org` is a `top-level namespace`; the root for all groups and projects of an organization
-- `gitlab` is a `project`; a project of the organization.
-
-The top-level namespace has served as the defacto Organization entity. With the creation of Organization, top-level namespaces will be [nested underneath Organizations](https://gitlab.com/gitlab-org/gitlab/-/issues/394796).
-
-Over time there won't be a distinction between a top level namespace and a group. All features that make Top-level namespaces different from groups will move to Organization.
-
-Discouraged synonyms: Root-level namespace
-
-![Term Top-level Namespace](images/term-top-level-namespace.png)
-
-#### Top-level namespace properties
-
-- Top-level namespaces belonging to an organization are located on the same Cell
-- Top-level namespaces can interact with other top-level namespaces that belong to the same organization
-
-### Users
-
-Users are available globally and not restricted to a single Cell. Users belong to a single organization, but can participate in many organizations through group and project membership with varying permissions. Inside organizations, users can create multiple top-level namespaces. User activity is not limited to a single organization but their contributions (for example TODOs) are only aggregated within an organization. This avoids the need for aggregating across cells.
-
-#### User properties
-
-- Users are shared globally across all Cells
-- Users can create multiple top-level namespaces
-- Users can be a member of multiple top-level namespaces
-- Users belong to one organization. See [!395736](https://gitlab.com/gitlab-org/gitlab/-/issues/395736)
-- Users can be members of groups and projects in different organizations
-- Users can administer organizations
-- User activity is aggregated in an organization
-- Every user has one personal namespace
-
-## Goals
-
-### Scalability
-
-The main goal of this new shared-infrastructure architecture is to provide additional scalability for our SaaS Platform. GitLab.com is largely monolithic and we have estimated (internal) that the current architecture has scalability limitations, even when database partitioning and decomposition are taken into account.
-
-Cells provide a horizontally scalable solution because additional Cells can be created based on demand. Cells can be provisioned and tuned as needed for optimal scalability.
-
-### Increased availability
-
-A major challenge for shared-infrastructure architectures is a lack of isolation between top-level namespaces. This can lead to noisy neighbor effects. A organization's behavior inside a top-level namespace can impact all other organizations. This is highly undesirable. Cells provide isolation at the cell level. A group of organizations is fully isolated from other organizations located on a different Cell. This minimizes noisy neighbor effects while still benefiting from the cost-efficiency of shared infrastructure.
-
-Additionally, Cells provide a way to implement disaster recovery capabilities. Entire Cells may be replicated to read-only standbys with automatic failover capabilities.
-
-### A consistent experience
-
-Organizations should have the same user experience on our SaaS platform as they do on a self-managed GitLab instance.
-
-### Regions
-
-GitLab.com is only hosted within the United States of America. Organizations located in other regions have voiced demand for local SaaS offerings. Cells provide a path towards [GitLab Regions](https://gitlab.com/groups/gitlab-org/-/epics/6037) because Cells may be deployed within different geographies. Depending on which of the organization's data is located outside a Cell, this may solve data residency and compliance problems.
-
-## Market segment
-
-Cells would provide a solution for organizations in the small to medium business (up to 100 users) and the mid-market segment (up to 2000 users).
-(See [segmentation definitions](https://about.gitlab.com/handbook/sales/field-operations/gtm-resources/#segmentation).)
-Larger organizations may benefit substantially from [GitLab Dedicated](../../../subscriptions/gitlab_dedicated/index.md).
-
-At this moment, GitLab.com has "social-network"-like capabilities that may not fit well into a more isolated organization model. Removing those features, however, possesses some challenges:
-
-1. How will existing `gitlab-org` contributors contribute to the namespace??
-1. How do we move existing top-level namespaces into the new model (effectively breaking their social features)?
-
-We should evaluate if the SMB and mid market segment is interested in these features, or if not having them is acceptable in most cases.
-
-### Self-managed
-
-For reasons of consistency, it is expected that self-managed instances will
-adopt the cells architecture as well. To expand, self-managed instances can
-continue with just a single Cell while supporting the option of adding additional
-Cells. Organizations, and possible User decomposition will also be adopted for
-self-managed instances.
-
-## High-level architecture problems to solve
-
-A number of technical issues need to be resolved to implement Cells (in no particular order). This section will be expanded.
-
-1. How are Cells provisioned? - [Design discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/396641)
-1. What is a Cells topology? - [Design discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/396641)
-1. How are users of an organization routed to the correct Cell? -
-1. How do users authenticate with Cells and Organizations? - [Design discussion](https://gitlab.com/gitlab-org/gitlab/-/issues/395736)
-1. How are Cells rebalanced?
-1. How can Cells implement disaster recovery capabilities?
-
-## Decision log
-
-- 2022-03-15: Google Cloud as the cloud service. [Reference](https://gitlab.com/gitlab-org/gitlab/-/issues/396641#note_1314932272)
-
-## Cross-section impact
-
-Cells is a fundamental architecture change that impacts other sections and stages. This section summarizes and links to other groups that may be impacted and highlights potential conflicts that need to be resolved. The Tenant Scale group is not responsible for achieving the goals of other groups but we want to ensure that dependencies are resolved.
-
-### Summary
-
-Based on discussions with other groups the net impact of introducing Cells and a new entity called organizations is mostly neutral. It may slow down development in some areas. We did not discover major blockers for other teams.
-
-1. We need to resolve naming conflicts (proposal is TBD)
-1. Cells requires introducing Organizations. Organizations are a new entity **above** top-level groups. Because this is a new entity, it may impact the ability to consolidate settings for Group::Organization and influence their decision on [how to approach introducing a an organization](https://gitlab.com/gitlab-org/gitlab/-/issues/376285#approach-2-organization-is-built-on-top-of-top-level-groups)
-1. Organizations may make it slightly easier for Fulfillment to realize their billing plans.
-
-### Impact on Group::Organization
-
-We synced with the Organization PM and Designer ([recording](https://youtu.be/b5Opn9cFWFk)) and discussed the similarities and differences between the Cells and Organization proposal ([presentation](https://docs.google.com/presentation/d/1FsUi22Up15b_tu6p2m-yLML3hCZ3rgrZrmzJAxUsNmU/edit?usp=sharing)).
-
-#### Goals of Group::Organization
-
-As defined in the [organization documentation](../../../user/organization/index.md):
-
-1. Create an entity to manage everything you do as a GitLab administrator, including:
- 1. Defining and applying settings to all of your groups, subgroups, and projects.
- 1. Aggregating data from all your groups, subgroups, and projects.
-1. Reach feature parity between SaaS and self-managed installations, with all Admin Area settings moving to groups (?). Hardware controls remain on the instance level.
-
-The [organization roadmap outlines](https://gitlab.com/gitlab-org/gitlab/-/issues/368237#high-level-goals) the current goals in detail.
-
-#### Potential conflicts with Cells
-
-- Organization defines a new entity as the primary organizational object for groups and projects.
-- We will only introduce one entity
-- Group::Organization highlighted the need to further validate the key assumption that users only care about what happens within their organization.
-
-### Impact on Fulfillment
-
-We synced with Fulfillment ([recording](https://youtu.be/FkQF3uF7vTY)) to discuss how Cells would impact them. Fulfillment is supportive of an entity above top-level namespaces. Their perspective is outline in [!5639](https://gitlab.com/gitlab-org/customers-gitlab-com/-/merge_requests/5639/diffs).
-
-#### Goals of Fulfillment
-
-- Fulfillment has a longstanding plan to move billing from the top-level namespace to a level above. This would mean that a license applies for an organization and all its top-level namespaces.
-- Fulfillment uses Zuora for billing and would like to have a 1-to-1 relationship between an organization and their Zuora entity called BillingAccount. They want to move away from tying a license to a single user.
-- If a customer needs multiple organizations, the corresponding BillingAccounts can be rolled up into a consolidated billing account (similar to [AWS consolidated billing](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/consolidated-billing.html))
-- Ideally, a self-managed instance has a single Organization by default, which should be enough for most customers.
-- Fulfillment prefers only one additional entity.
-
-A rough representation of this is:
-
-![Cells and Fulfillment](images/pods-and-fulfillment.png)
-
-#### Potential conflicts with Cells
-
-- There are no known conflicts between Fulfillment's plans and Cells
+- [Glossary](glossary.md)
+- [Goals](goals.md)
+- [Cross-section impact](impact.md)
## Iteration plan
@@ -275,6 +67,10 @@ This is the list of known affected features with the proposed solutions.
- [Cells: GitLab Pages](cells-feature-gitlab-pages.md)
- [Cells: Agent for Kubernetes](cells-feature-agent-for-kubernetes.md)
+## Decision log
+
+- 2022-03-15: Google Cloud as the cloud service. For details, see [issue 396641](https://gitlab.com/gitlab-org/gitlab/-/issues/396641#note_1314932272).
+
## Links
- [Internal Pods presentation](https://docs.google.com/presentation/d/1x1uIiN8FR9fhL7pzFh9juHOVcSxEY7d2_q4uiKKGD44/edit#slide=id.ge7acbdc97a_0_155)
diff --git a/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md b/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md
index ebf08c88ef6..f352fea84b1 100644
--- a/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md
+++ b/doc/architecture/blueprints/cells/proposal-stateless-router-with-buffering-requests.md
@@ -28,7 +28,7 @@ databases to be replicated across regions.
Users are not directly exposed to the concept of Cells but instead they see
different data dependent on their chosen "organization".
-[Organizations](index.md#organizations) will be a new model introduced to enforce isolation in the
+[Organizations](glossary.md#organizations) will be a new model introduced to enforce isolation in the
application and allow us to decide which request route to which cell, since an
organization can only be on a single cell.
diff --git a/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md b/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md
index 7602a9d3f4d..aadc08016e3 100644
--- a/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md
+++ b/doc/architecture/blueprints/cells/proposal-stateless-router-with-routes-learning.md
@@ -28,7 +28,7 @@ databases to be replicated across regions.
Users are not directly exposed to the concept of Cells but instead they see
different data dependent on their chosen "organization".
-[Organizations](index.md#organizations) will be a new model introduced to enforce isolation in the
+[Organizations](glossary.md#organizations) will be a new model introduced to enforce isolation in the
application and allow us to decide which request route to which cell, since an
organization can only be on a single cell.
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 3f1c72c2bd6..ec361c8b30f 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -244,6 +244,11 @@ While troubleshooting CI/CD job token authentication issues, be aware that:
- A [GraphQL example mutation](../../api/graphql/getting_started.md#update-project-settings)
is available to toggle the scope settings per project.
+- [This comment](https://gitlab.com/gitlab-org/gitlab/-/issues/351740#note_1335673157)
+ demonstrates how to use graphQL with Bash and cURL to:
+ - Enable the inbound token access scope.
+ - Give access to project B from project A, or add B to A's allowlist.
+ - To remove project access.
- When the [CI/CD job token scopes](#configure-cicd-job-token-access) are enabled,
and the job token is being used to access a different project:
- The user that executes the job must be a member of the project that is being accessed.
diff --git a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
index bf4d750423d..a1004f5b6b2 100644
--- a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
+++ b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
@@ -529,3 +529,19 @@ end
```
When running mobile tests for phone layouts, both `remote_mobile_device_name` and `mobile_layout` are `true` but when using a tablet layout, only `remote_mobile_device_name` is true. This is because phone layouts have more menus closed by default such as how both tablets and phones have the left nav closed but unlike phone layouts, tablets have the regular top navigation bar, not the mobile one. So in the case where the navigation being edited needs to be used in tablet layouts as well, use `remote_mobile_device_name` instead of `mobile_layout?` when prepending so it will use it if it's a tablet layout as well.
+
+## OpenID Connect (OIDC) tests
+
+To run the [`login_via_oidc_with_gitlab_as_idp_spec`](https://gitlab.com/gitlab-org/gitlab/-/blob/188e2c876a17a097448d7f3ed35bdf264fed0d3b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb) on your local machine:
+
+1. Make sure your GDK is set to run on a non-localhost address such as `gdk.test:3000`.
+1. Configure a [loopback interface to 172.16.123.1](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/6fe7b46403229f12ab6d903f99b024e0b82cb94a/doc/howto/local_network.md#create-loopback-interface).
+1. Make sure Docker Desktop or Rancher Desktop is running.
+1. Add an entry to your `/etc/hosts` file for `gitlab-oidc-consumer.bridge` pointing to `127.0.0.1`.
+1. From the `qa` directory, run the following command. To set the GitLab image you want to use, update the `RELEASE` variable. For example, to use the latest EE image, set `RELEASE` to `gitlab/gitlab-ee:latest`:
+
+ ```shell
+ bundle install
+
+ RELEASE_REGISTRY_URL='registry.gitlab.com' RELEASE_REGISTRY_USERNAME='<your_gitlab_username>' RELEASE_REGISTRY_PASSWORD='<your_gitlab_personal_access_token>' RELEASE='registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:1d5a644145dfe901ea7648d825f8f9f3006d0acf' GITLAB_QA_ADMIN_ACCESS_TOKEN="<your_gdk_admin_personal_access_token>" QA_DEBUG=true CHROME_HEADLESS=false bundle exec bin/qa Test::Instance::All http://gdk.test:3000 qa/specs/features/browser_ui/1_manage/login/login_via_oidc_with_gitlab_as_idp_spec.rb
+ ```
diff --git a/doc/development/value_stream_analytics.md b/doc/development/value_stream_analytics.md
index 5f500640841..43db2187cdd 100644
--- a/doc/development/value_stream_analytics.md
+++ b/doc/development/value_stream_analytics.md
@@ -344,9 +344,25 @@ Seed issues and merge requests for value stream analytics:
Seed DORA daily metrics for value stream, insights and CI/CD analytics:
-1. [Create an environment from the UI](../ci/environments/index.md#create-a-static-environment) named `production`.
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the project's homepage, in the upper-left corner, copy the **Project ID**. You need it in a later step.
+1. [Create an environment for your selected project from the UI](../ci/environments/index.md#create-a-static-environment) named `production`.
1. Open the rails console:
```shell
rails c
```
+
+1. In the rails console, find the created environment by searching for the project ID:
+
+ ```shell
+ e = Environment.find_by(project_id: <project-id>, name: "production")
+ ```
+
+1. To seed data for the past 100 days for the environment, run the following command:
+
+ ```shell
+ 100.times { |i| Dora::DailyMetrics.create(environment_id: e.id, date: (i + 1).days.ago, deployment_frequency: rand(50), incidents_count: rand(5), lead_time_for_changes_in_seconds: rand(50000), time_to_restore_service_in_seconds: rand(100000)) }
+ ```
+
+DORA metric data should now be available for your selected project and any group or subgroup it belongs to.
diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md
index 9ae97c22919..bc0381a5216 100644
--- a/doc/integration/jira/development_panel.md
+++ b/doc/integration/jira/development_panel.md
@@ -34,7 +34,7 @@ The information displayed in the Jira development panel depends on where you men
| GitLab: where you mention the Jira issue ID | Jira development panel: what information is displayed |
|------------------------------------------------|-------------------------------------------------------|
-| Merge request title or description | Link to the merge request<br>[GitLab 15.11 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/354373): Link to the branch |
+| Merge request title or description | Link to the merge request<br>Link to the branch ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11) |
| Branch name | Link to the branch |
| Commit message | Link to the commit |
| [Jira Smart Commit](#jira-smart-commits) | Custom comment, logged time, or workflow transition |
diff --git a/doc/policy/alpha-beta-support.md b/doc/policy/alpha-beta-support.md
index 1d5bacf5a2b..8976e0ed503 100644
--- a/doc/policy/alpha-beta-support.md
+++ b/doc/policy/alpha-beta-support.md
@@ -14,23 +14,23 @@ All other features are considered to be Generally Available (GA).
Support is not provided for features listed as "Experimental" or "Alpha" or any similar designation. Issues regarding such features should be opened in the GitLab issue tracker.
-- Not ready for production use
-- No support available
-- May be unstable or have performance issues
-- Can be removed at any time
-- Data loss may occur
-- Documentation may not exist or just be in a blog format
+- Not ready for production use.
+- No support available.
+- May be unstable or have performance issues.
+- Can be removed at any time.
+- Data loss may occur.
+- Documentation may not exist or just be in a blog format.
- Behind a feature flag that is on by default and the UI reflects Experiment status.
- Behind a toggle that is off by default and the UI reflects Experiment status.
-- Feedback issue to engage with team
-- UX not finalized, might be just quick action access
-- Not announced in a release post
+- Feedback issue to engage with team.
+- UX not finalized, might be just quick action access.
+- Not announced in a release post.
## Beta
Commercially-reasonable efforts are made to provide limited support for features designated as "Beta," with the expectation that issues require extra time and assistance from development to troubleshoot.
-- May not be ready for production use and the UI and documentation will reflect this status
+- May not be ready for production use and the UI and documentation will reflect this status.
- May be unstable and can cause performance and stability issues.
- Configuration and dependencies unlikely to change.
- Features and functions unlikely to change. However, breaking changes may occur outside of major releases or with less notice than for Generally Available features.
diff --git a/doc/tutorials/convert_personal_namespace_into_group.md b/doc/tutorials/convert_personal_namespace_into_group.md
index e272d854a35..6018d85a466 100644
--- a/doc/tutorials/convert_personal_namespace_into_group.md
+++ b/doc/tutorials/convert_personal_namespace_into_group.md
@@ -1,6 +1,6 @@
---
-stage: none
-group: Tutorials
+stage: Data Stores
+group: Tenant Scale
info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
---
diff --git a/doc/tutorials/move_personal_project_to_a_group.md b/doc/tutorials/move_personal_project_to_a_group.md
index e3ab1e8fbd8..22d7005847b 100644
--- a/doc/tutorials/move_personal_project_to_a_group.md
+++ b/doc/tutorials/move_personal_project_to_a_group.md
@@ -1,6 +1,6 @@
---
-stage: none
-group: Tutorials
+stage: Data Stores
+group: Tenant Scale
info: For assistance with this tutorial, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-other-projects-and-subjects.
---
diff --git a/doc/user/project/integrations/apple_app_store.md b/doc/user/project/integrations/apple_app_store.md
index e326e7a222b..bef5102710d 100644
--- a/doc/user/project/integrations/apple_app_store.md
+++ b/doc/user/project/integrations/apple_app_store.md
@@ -35,8 +35,9 @@ GitLab supports enabling the Apple App Store integration at the project level. C
After the Apple App Store integration is activated:
-- The global variables `$APP_STORE_CONNECT_API_KEY_ISSUER_ID`, `$APP_STORE_CONNECT_API_KEY_KEY_ID`, and `$APP_STORE_CONNECT_API_KEY_KEY` are created for CI/CD use.
+- The global variables `$APP_STORE_CONNECT_API_KEY_ISSUER_ID`, `$APP_STORE_CONNECT_API_KEY_KEY_ID`, `$APP_STORE_CONNECT_API_KEY_KEY`, and `$APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64` are created for CI/CD use.
- `$APP_STORE_CONNECT_API_KEY_KEY` contains the Base64 encoded Private Key.
+- `$APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64` is always `true`.
## Security considerations
@@ -51,7 +52,5 @@ Malicious code pushed to your `.gitlab-ci.yml` file could compromise your variab
Because this integration works out of the box with fastlane adding the code below to an app's `fastlane/Fastfile` activates the integration, and create the connection for any interactions with the Apple App Store uploading a Test Flight or public App Store release.
```ruby
-app_store_connect_api_key(
- is_key_content_base64: true
-)
+app_store_connect_api_key
```
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index f9e3f1b9225..c24526ddd86 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -136,7 +136,7 @@ To do so:
1. Fill in the name field. You can't specify a description if creating a label this way.
You can add a description later by [editing the label](#edit-a-label).
1. Select a color by selecting from the available colors, or enter a hex color value for a specific color.
-1. Select **Create**.
+1. Select **Create**. Your label is created and selected.
### Create a group label
diff --git a/lib/api/entities/user_preferences.rb b/lib/api/entities/user_preferences.rb
index ceee6c610d3..e66e83549b2 100644
--- a/lib/api/entities/user_preferences.rb
+++ b/lib/api/entities/user_preferences.rb
@@ -3,7 +3,7 @@
module API
module Entities
class UserPreferences < Grape::Entity
- expose :id, :user_id, :view_diffs_file_by_file, :show_whitespace_in_diffs
+ expose :id, :user_id, :view_diffs_file_by_file, :show_whitespace_in_diffs, :pass_user_identities_to_ci_jwt
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e18a16f384a..48e13127712 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -1227,7 +1227,7 @@ module API
attrs = declared_params(include_missing: false)
- service = ::Users::UpsertCreditCardValidationService.new(attrs, user).execute
+ service = ::Users::UpsertCreditCardValidationService.new(attrs).execute
if service.success?
present user.credit_card_validation, with: Entities::UserCreditCardValidations
@@ -1243,7 +1243,8 @@ module API
params do
optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page'
optional :show_whitespace_in_diffs, type: Boolean, desc: 'Flag indicating the user sees whitespace changes in diffs'
- at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs
+ optional :pass_user_identities_to_ci_jwt, type: Boolean, desc: 'Flag indicating the user passes their external identities as CI information'
+ at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs, :pass_user_identities_to_ci_jwt
end
put "preferences", feature_category: :user_profile, urgency: :high do
authenticate!
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index eb071b44374..ee3d4b6f989 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -57,7 +57,7 @@ module Gitlab
gon.current_user_fullname = current_user.name
gon.current_user_avatar_url = current_user.avatar_url
gon.time_display_relative = current_user.time_display_relative
- gon.use_new_navigation = Feature.enabled?(:super_sidebar_nav, current_user) && current_user&.use_new_navigation
+ gon.use_new_navigation = NavHelper.show_super_sidebar?(current_user)
end
# Initialize gon.features with any flags that should be
diff --git a/lib/slack/api.rb b/lib/slack/api.rb
new file mode 100644
index 00000000000..0775e783337
--- /dev/null
+++ b/lib/slack/api.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Client for interacting with the Slack API.
+# See https://api.slack.com/web.
+module Slack
+ class API
+ BASE_URL = 'https://slack.com/api'
+ BASE_HEADERS = { 'Content-Type' => 'application/json; charset=utf-8' }.freeze
+
+ def initialize(slack_installation)
+ @token = slack_installation.bot_access_token
+
+ raise ArgumentError, "No token for slack installation #{slack_installation.id}" unless @token
+ end
+
+ def post(api_method, payload)
+ url = "#{BASE_URL}/#{api_method}"
+ headers = BASE_HEADERS.merge('Authorization' => "Bearer #{token}")
+
+ Gitlab::HTTP.post(url, body: payload.to_json, headers: headers)
+ end
+
+ private
+
+ attr_reader :token
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6f958831f98..b1f7f858099 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -859,6 +859,9 @@ msgstr ""
msgid "%{linkStart} Learn more%{linkEnd}."
msgstr ""
+msgid "%{linkStart}Approval settings%{linkEnd} prevent approvals by its eligible approvers."
+msgstr ""
+
msgid "%{listToShow}, and %{awardsListLength} more"
msgstr ""
@@ -1305,15 +1308,6 @@ msgstr ""
msgid "'%{level}' is not a valid visibility level"
msgstr ""
-msgid "'%{name}' Value Stream created"
-msgstr ""
-
-msgid "'%{name}' Value Stream deleted"
-msgstr ""
-
-msgid "'%{name}' Value Stream saved"
-msgstr ""
-
msgid "'%{source}' is not a import source"
msgstr ""
@@ -5502,39 +5496,9 @@ msgstr ""
msgid "Approvals required"
msgstr ""
-msgid "Approvals|%{count} of %{total}"
-msgstr ""
-
-msgid "Approvals|Action required"
-msgstr ""
-
-msgid "Approvals|Auto approved"
-msgstr ""
-
-msgid "Approvals|It looks like there was a conflict between the rules for approving this Merge Request and the users who were eligible to approve it. As a result, the system has automatically approved it to keep things moving."
-msgstr ""
-
-msgid "Approvals|Rule automatically approved"
-msgstr ""
-
-msgid "Approvals|Rule cannot be approved"
-msgstr ""
-
msgid "Approvals|Section: %section"
msgstr ""
-msgid "Approvals|The number of people who need to approve this is more than those who are allowed to. Please ask the project owner to update %{securityPolicy}."
-msgstr ""
-
-msgid "Approvals|Verify the number of %{linkStart}eligible security approvers%{linkEnd} matches the required approvers for the security policy."
-msgstr ""
-
-msgid "Approvals|Verify your %{eligibleApproverLinkStart}eligible approvers%{eligibleApproverLinkEnd} and %{approvalSettingsLinkStart}approval settings%{approvalSettingsLinkEnd} agree with each other."
-msgstr ""
-
-msgid "Approvals|Verify your %{linkStart}approval settings%{linkEnd} do not conflict with this rule."
-msgstr ""
-
msgid "Approve"
msgstr ""
@@ -9107,9 +9071,6 @@ msgstr ""
msgid "Child issues and epics"
msgstr ""
-msgid "Child removed"
-msgstr ""
-
msgid "Chinese language support using"
msgstr ""
@@ -11852,9 +11813,6 @@ msgstr ""
msgid "ContributionAnalytics|The given date range is larger than %{number_of_days} days"
msgstr ""
-msgid "ContributionAnalytics|The given date range is larger than 93 days"
-msgstr ""
-
msgid "ContributionAnalytics|The to date is earlier than the given from date"
msgstr ""
@@ -12353,9 +12311,6 @@ msgstr ""
msgid "Create new %{name} by email"
msgstr ""
-msgid "Create new Value Stream"
-msgstr ""
-
msgid "Create new branch"
msgstr ""
@@ -13084,9 +13039,6 @@ msgstr ""
msgid "CycleAnalytics|Stage time: %{title}"
msgstr ""
-msgid "CycleAnalytics|Tasks by type"
-msgstr ""
-
msgid "CycleAnalytics|The average time items spent in this stage. Data limited to items completed within this date range."
msgstr ""
@@ -15892,9 +15844,6 @@ msgstr ""
msgid "Edit image description"
msgstr ""
-msgid "Edit image text or link"
-msgstr ""
-
msgid "Edit in pipeline editor"
msgstr ""
@@ -17496,9 +17445,6 @@ msgstr ""
msgid "Existing sign in methods may be removed"
msgstr ""
-msgid "Exit."
-msgstr ""
-
msgid "Expand"
msgstr ""
@@ -21869,12 +21815,6 @@ msgstr ""
msgid "IdentityVerification|A new code has been sent."
msgstr ""
-msgid "IdentityVerification|Before you create your first project, we need you to verify your identity with a valid payment method. You will not be charged during this step. If we ever need to charge you, we will let you know."
-msgstr ""
-
-msgid "IdentityVerification|Before you create your group, we need you to verify your identity with a valid payment method. You will not be charged during this step. If we ever need to charge you, we will let you know."
-msgstr ""
-
msgid "IdentityVerification|Before you finish creating your account, we need to verify your identity. On the verification page, enter the following code."
msgstr ""
@@ -21887,9 +21827,6 @@ msgstr ""
msgid "IdentityVerification|Confirm your email address"
msgstr ""
-msgid "IdentityVerification|Create a project"
-msgstr ""
-
msgid "IdentityVerification|Didn't receive a code?"
msgstr ""
@@ -22013,9 +21950,6 @@ msgstr ""
msgid "IdentityVerification|We've sent a verification code to +%{phoneNumber}"
msgstr ""
-msgid "IdentityVerification|You can always verify your account at a later time to create a group."
-msgstr ""
-
msgid "IdentityVerification|You will receive a text containing a code. Standard charges may apply."
msgstr ""
@@ -23770,6 +23704,9 @@ msgstr ""
msgid "Introducing Your DevOps Reports"
msgstr ""
+msgid "Invalid"
+msgstr ""
+
msgid "Invalid Insights config file detected"
msgstr ""
@@ -23833,6 +23770,9 @@ msgstr ""
msgid "Invalid repository path"
msgstr ""
+msgid "Invalid rules are automatically approved to unblock the merge request."
+msgstr ""
+
msgid "Invalid server response"
msgstr ""
@@ -24465,6 +24405,9 @@ msgstr ""
msgid "Issue|Title"
msgstr ""
+msgid "It doesn't have any %{linkStart}eligible approvers%{linkEnd}."
+msgstr ""
+
msgid "It looks like you're attempting to activate your subscription. Use %{a_start}the Subscription page%{a_end} instead."
msgstr ""
@@ -30752,9 +30695,6 @@ msgstr ""
msgid "Only effective when remote storage is enabled. Set to 0 for no size limit."
msgstr ""
-msgid "Only group members with at least the Reporter role can view or be notified about this epic"
-msgstr ""
-
msgid "Only include features new to your current subscription tier."
msgstr ""
@@ -31518,9 +31458,6 @@ msgstr ""
msgid "PackageRegistry|There are no packages yet"
msgstr ""
-msgid "PackageRegistry|There are security risks if packages are deleted while request forwarding is enabled."
-msgstr ""
-
msgid "PackageRegistry|There are security risks if packages are deleted while request forwarding is enabled. %{docLinkStart}What are the risks?%{docLinkEnd}"
msgstr ""
@@ -33428,9 +33365,6 @@ msgstr ""
msgid "Prevent auto-stopping"
msgstr ""
-msgid "Prevent editing approval rules in projects and merge requests"
-msgstr ""
-
msgid "Prevent environment from auto-stopping"
msgstr ""
@@ -36557,9 +36491,6 @@ msgstr ""
msgid "RegistrationFeatures|Read more about the %{link_start}Registration Features Program%{link_end}."
msgstr ""
-msgid "RegistrationFeatures|Registration Features Program"
-msgstr ""
-
msgid "RegistrationFeatures|Want to %{feature_title} for free?"
msgstr ""
@@ -38930,10 +38861,10 @@ msgstr ""
msgid "ScanExecutionPolicy|%{rules} actions for the %{scopes} %{branches} %{agents} %{namespaces}"
msgstr ""
-msgid "ScanExecutionPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require a %{scan} scan on runner that %{tags}"
+msgid "ScanExecutionPolicy|%{thenLabelStart}Then%{thenLabelEnd} Run a %{scan} scan on runner that %{tags}"
msgstr ""
-msgid "ScanExecutionPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require a %{scan} scan to run %{dastProfiles} with tags %{tags}"
+msgid "ScanExecutionPolicy|%{thenLabelStart}Then%{thenLabelEnd} Run a %{scan} scan with %{dastProfiles} with tags %{tags}"
msgstr ""
msgid "ScanExecutionPolicy|A pipeline is run"
@@ -41449,9 +41380,6 @@ msgstr ""
msgid "Showing version #%{versionNumber}"
msgstr ""
-msgid "Shows issues for group '%{group_name}' from Nov 1, 2019 to Dec 31, 2019"
-msgstr ""
-
msgid "Side-by-side"
msgstr ""
@@ -43707,6 +43635,9 @@ msgstr ""
msgid "Target branch or tag"
msgstr ""
+msgid "Target branch: %{target_branch}"
+msgstr ""
+
msgid "Target roles"
msgstr ""
@@ -45638,6 +45569,9 @@ msgstr ""
msgid "This repository was last checked %{last_check_timestamp}. The check passed."
msgstr ""
+msgid "This rule is invalid because no one can approve it for one or more of these reasons:"
+msgstr ""
+
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
@@ -46875,9 +46809,6 @@ msgstr ""
msgid "Two-factor authentication has been disabled for your GitLab account."
msgstr ""
-msgid "Two-factor authentication has been disabled successfully for %{user_email}!"
-msgstr ""
-
msgid "Two-factor authentication has been disabled successfully for %{username}!"
msgstr ""
@@ -49868,6 +49799,9 @@ msgstr ""
msgid "Why can't I approve?"
msgstr ""
+msgid "Why is this rule invalid?"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -51536,9 +51470,6 @@ msgstr ""
msgid "Your top-level group %{namespace_name} will move to a read-only state soon"
msgstr ""
-msgid "Your top-level group %{name} will move to a read-only state soon"
-msgstr ""
-
msgid "Your top-level group is over the user and storage limits and has been placed in a read-only state."
msgstr ""
@@ -51785,9 +51716,6 @@ msgstr ""
msgid "can only have one escalation policy"
msgstr ""
-msgid "can't be blank"
-msgstr ""
-
msgid "can't be enabled when delayed group deletion is disabled"
msgstr ""
@@ -52958,12 +52886,6 @@ msgstr ""
msgid "mrWidget|%{boldStart}Ready to be merged automatically.%{boldEnd} Ask someone with write access to this repository to merge this request."
msgstr ""
-msgid "mrWidget|%{dangerStart}%{rules} rule can't be approved%{dangerEnd}"
-msgstr ""
-
-msgid "mrWidget|%{dangerStart}%{rules} rules can't be approved%{dangerEnd}"
-msgstr ""
-
msgid "mrWidget|%{mergeError}."
msgstr ""
@@ -52979,10 +52901,10 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|%{rules} invalid rule has been approved automatically"
+msgid "mrWidget|%{rules} invalid rule has been approved automatically, as no one can approve it."
msgstr ""
-msgid "mrWidget|%{rules} invalid rules have been approved automatically"
+msgid "mrWidget|%{rules} invalid rules have been approved automatically, as no one can approve them."
msgstr ""
msgid "mrWidget|A merge train is a queued list of merge requests waiting to be merged into the target branch. The changes in each merge request are combined with the changes in earlier merge requests and tested before merge."
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index d923a659da0..18efa445b26 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -27,7 +27,7 @@ gitlab:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average is around 0.100vCPU (setting request accordingly)
+ # The average seems to be around 0.100vCPU (setting request accordingly). Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.196vCPU (setting limit accordingly)
cpu: 150m
@@ -53,7 +53,7 @@ gitlab:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average is around 0.01vCPU (setting request accordingly)
+ # The average seems to be around 0.01vCPU (setting request accordingly). Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.127vCPU (setting limit accordingly)
cpu: 10m
@@ -105,7 +105,7 @@ gitlab:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average seems to be around 0.100vCPU
+ # The average seems to be around 0.100vCPU. Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.250vCPU (setting limit accordingly)
cpu: 150m
@@ -149,14 +149,14 @@ gitlab-runner:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average seems to be around 0.01vCPU
+ # The average seems to be around 0.01vCPU. Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.015vCPU (setting limit accordingly)
cpu: 10m
memory: 100Mi
limits:
# In case somebody would like to use runners in review-apps, we set the limit higher than the requests
- cpu: 400m
+ cpu: 1015m
memory: 150Mi
nodeSelector:
preemptible: "true"
@@ -186,7 +186,7 @@ nginx-ingress:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average seems to be around 0.02vCPU
+ # The average seems to be around 0.02vCPU. Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.07vCPU (setting limit accordingly)
cpu: 10m
@@ -222,7 +222,7 @@ postgresql:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average seems to be around 0.150vCPU
+ # The average seems to be around 0.150vCPU. Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.420vCPU (setting limit accordingly)
cpu: 150m
@@ -248,7 +248,7 @@ redis:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average seems to be around 0.03vCPU
+ # The average seems to be around 0.03vCPU. Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.500vCPU (setting limit accordingly)
cpu: 10m
@@ -271,7 +271,7 @@ registry:
#
# Data over the 3 months (2023-02-24 - 2023-04-19)
#
- # The average seems to be around 0.0005vCPU
+ # The average seems to be around 0.0005vCPU. Note that this is a guesstimate based on the chart.
#
# The maximum CPU usage was 0.0.003vCPU (setting limit accordingly)
requests:
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index d765e5562b6..0b940c552e5 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -43,6 +43,29 @@ FactoryBot.define do
end
end
+ factory :gitlab_slack_application_integration, class: 'Integrations::GitlabSlackApplication' do
+ project
+ active { true }
+ type { 'Integrations::GitlabSlackApplication' }
+ slack_integration { association :slack_integration, integration: instance }
+
+ transient do
+ all_channels { true }
+ end
+
+ after(:build) do |integration, evaluator|
+ next unless evaluator.all_channels
+
+ integration.event_channel_names.each do |name|
+ integration.send("#{name}=".to_sym, "##{name}")
+ end
+ end
+
+ trait :all_features_supported do
+ slack_integration { association :slack_integration, :all_features_supported, integration: instance }
+ end
+ end
+
factory :packagist_integration, class: 'Integrations::Packagist' do
project
type { 'Integrations::Packagist' }
diff --git a/spec/factories/slack_integrations.rb b/spec/factories/slack_integrations.rb
new file mode 100644
index 00000000000..a43ba8e7453
--- /dev/null
+++ b/spec/factories/slack_integrations.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :slack_integration do
+ sequence(:team_id) { |n| "T123#{n}" }
+ sequence(:user_id) { |n| "U123#{n}" }
+ sequence(:bot_user_id) { |n| "U123#{n}" }
+ sequence(:bot_access_token) { |n| OpenSSL::Digest::SHA256.hexdigest(n.to_s) }
+ sequence(:team_name) { |n| "team#{n}" }
+ sequence(:alias) { |n| "namespace#{n}/project_name#{n}" }
+
+ integration { association :gitlab_slack_application_integration, slack_integration: instance }
+
+ trait :legacy do
+ bot_user_id { nil }
+ bot_access_token { nil }
+ end
+
+ trait :all_features_supported do
+ after(:build) do |slack_integration, _evaluator|
+ slack_integration.authorized_scope_names = %w[commands chat:write chat:write.public]
+ end
+ end
+ end
+end
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 64111cfb01a..5a976816f74 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
-import * as urlUtility from '~/lib/utils/url_utility';
+import { updateHistory } from '~/lib/utils/url_utility';
import {
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_LABEL,
@@ -23,6 +23,12 @@ import { createStore } from '~/boards/stores';
Vue.use(Vuex);
+jest.mock('~/lib/utils/url_utility', () => ({
+ updateHistory: jest.fn(),
+ setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams,
+ queryToObject: jest.requireActual('~/lib/utils/url_utility').queryToObject,
+}));
+
describe('BoardFilteredSearch', () => {
let wrapper;
let store;
@@ -88,10 +94,9 @@ describe('BoardFilteredSearch', () => {
});
it('calls historyPushState', () => {
- jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(updateHistory).toHaveBeenCalledWith({
replace: true,
title: '',
url: 'http://test.host/',
@@ -120,7 +125,7 @@ describe('BoardFilteredSearch', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(wrapper.vm, 'performSearch').mockImplementation();
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
it('sets the url params to the correct results', () => {
@@ -137,10 +142,11 @@ describe('BoardFilteredSearch', () => {
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: '!=' } },
];
- jest.spyOn(urlUtility, 'updateHistory');
+
findFilteredSearch().vm.$emit('onFilter', mockFilters);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(store.dispatch).toHaveBeenCalledWith('performSearch');
+ expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url:
@@ -158,10 +164,10 @@ describe('BoardFilteredSearch', () => {
const mockFilters = [
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: assigneeParam, operator: '=' } },
];
- jest.spyOn(urlUtility, 'updateHistory');
+
findFilteredSearch().vm.$emit('onFilter', mockFilters);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url: expected,
@@ -175,8 +181,6 @@ describe('BoardFilteredSearch', () => {
createComponent({
initialFilterParams: { authorUsername: 'root', labelName: ['label'], healthStatus: 'Any' },
});
-
- jest.spyOn(store, 'dispatch');
});
it('passes the correct props to FilterSearchBar', () => {
@@ -194,11 +198,9 @@ describe('BoardFilteredSearch', () => {
});
it('emits setFilters and updates URL when onFilter is emitted', () => {
- jest.spyOn(urlUtility, 'updateHistory');
-
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url: 'http://test.host/',
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index a28d5a278e6..506db4144fc 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -186,6 +186,19 @@ comment -->
);
});
+ it('correctly renders a comment with markdown in it without adding any slashes', () => {
+ expect(serialize(paragraph('hi'), comment('this is a list\n- a\n- b\n- c'))).toBe(
+ `
+hi
+
+<!--this is a list
+- a
+- b
+- c-->
+ `.trim(),
+ );
+ });
+
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index ebf4771e97f..0187970efdc 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
@@ -78,6 +78,7 @@ describe('IssuesDashboardApp component', () => {
}
const findCalendarButton = () => wrapper.findByRole('link', { name: i18n.calendarLabel });
+ const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
@@ -113,11 +114,10 @@ describe('IssuesDashboardApp component', () => {
});
describe('UI components', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent();
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ await waitForPromises();
});
// https://gitlab.com/gitlab-org/gitlab/-/issues/391722
@@ -154,12 +154,24 @@ describe('IssuesDashboardApp component', () => {
});
});
- it('renders RSS button link', () => {
- expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
- });
+ describe('actions dropdown', () => {
+ it('renders', () => {
+ expect(findDisclosureDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'ellipsis_v',
+ noCaret: true,
+ textSrOnly: true,
+ toggleText: 'Actions',
+ });
+ });
- it('renders calendar button link', () => {
- expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ it('renders RSS button link', () => {
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
+ });
+
+ it('renders calendar button link', () => {
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ });
});
it('renders issue time information', () => {
@@ -174,11 +186,10 @@ describe('IssuesDashboardApp component', () => {
describe('fetching issues', () => {
describe('with a search query', () => {
describe('when there are issues returned', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent();
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ await waitForPromises();
});
it('renders the issues', () => {
@@ -193,12 +204,12 @@ describe('IssuesDashboardApp component', () => {
});
describe('when there are no issues returned', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent({
issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse),
});
- return waitForPromises();
+ await waitForPromises();
});
it('renders no issues', () => {
@@ -218,10 +229,10 @@ describe('IssuesDashboardApp component', () => {
describe('with no search query', () => {
let issuesQueryHandler;
- beforeEach(() => {
+ beforeEach(async () => {
issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse);
mountComponent({ issuesQueryHandler });
- return waitForPromises();
+ await waitForPromises();
});
it('does not call issues query', () => {
@@ -307,11 +318,10 @@ describe('IssuesDashboardApp component', () => {
${'fetching issues'} | ${'issuesQueryHandler'} | ${i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'issuesCountsQueryHandler'} | ${i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent({ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')) });
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ await waitForPromises();
});
it('shows an error message', () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
index e9023cb9ff6..c0417a2a40b 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
@@ -173,6 +173,21 @@ describe('DropdownContent', () => {
expect(findCreateView().exists()).toBe(false);
expect(findLabelsView().exists()).toBe(true);
});
+
+ it('selects created labels', async () => {
+ const createdLabel = {
+ id: 29,
+ title: 'new label',
+ description: null,
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ };
+
+ findCreateView().vm.$emit('labelCreated', createdLabel);
+ await nextTick();
+
+ expect(findLabelsView().props('localSelectedLabels')).toContain(createdLabel);
+ });
});
describe('Labels view', () => {
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index adf784360c2..d1e3c7d2240 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe NavHelper do
+RSpec.describe NavHelper, feature_category: :navigation do
describe '#header_links' do
include_context 'custom session'
@@ -136,23 +136,8 @@ RSpec.describe NavHelper do
end
describe '#show_super_sidebar?' do
- shared_examples '#show_super_sidebar returns false' do
- it 'returns false' do
- expect(helper.show_super_sidebar?).to eq(false)
- end
- end
-
- it 'returns false by default' do
- allow(helper).to receive(:current_user).and_return(nil)
-
- expect(helper.show_super_sidebar?).to be_falsy
- end
-
- context 'when used is signed-in' do
- let_it_be(:user) { create(:user) }
-
+ shared_examples 'show_super_sidebar is supposed to' do
before do
- allow(helper).to receive(:current_user).and_return(user)
stub_feature_flags(super_sidebar_nav: new_nav_ff)
user.update!(use_new_navigation: user_preference)
end
@@ -163,13 +148,13 @@ RSpec.describe NavHelper do
context 'when user has new nav disabled' do
let(:user_preference) { false }
- it_behaves_like '#show_super_sidebar returns false'
+ specify { expect(subject).to eq false }
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
- it_behaves_like '#show_super_sidebar returns false'
+ specify { expect(subject).to eq false }
end
end
@@ -179,17 +164,47 @@ RSpec.describe NavHelper do
context 'when user has new nav disabled' do
let(:user_preference) { false }
- it_behaves_like '#show_super_sidebar returns false'
+ specify { expect(subject).to eq false }
end
context 'when user has new nav enabled' do
let(:user_preference) { true }
- it 'returns true' do
- expect(helper.show_super_sidebar?).to eq(true)
- end
+ specify { expect(subject).to eq true }
end
end
end
+
+ context 'when nil is provided' do
+ specify { expect(helper.show_super_sidebar?(nil)).to eq false }
+ end
+
+ context 'when no user is signed-in' do
+ specify do
+ allow(helper).to receive(:current_user).and_return(nil)
+
+ expect(helper.show_super_sidebar?).to eq false
+ end
+ end
+
+ context 'when user is signed-in' do
+ let_it_be(:user) { create(:user) }
+
+ context 'with current_user as a default' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ subject { helper.show_super_sidebar? }
+
+ it_behaves_like 'show_super_sidebar is supposed to'
+ end
+
+ context 'with user provided as an argument' do
+ subject { helper.show_super_sidebar?(user) }
+
+ it_behaves_like 'show_super_sidebar is supposed to'
+ end
+ end
end
end
diff --git a/spec/lib/slack/api_spec.rb b/spec/lib/slack/api_spec.rb
new file mode 100644
index 00000000000..360e0284164
--- /dev/null
+++ b/spec/lib/slack/api_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Slack::API, feature_category: :integrations do
+ describe '#post' do
+ let(:slack_installation) { build(:slack_integration) }
+ let(:api_method) { 'api_method_call' }
+ let(:api_url) { "#{described_class::BASE_URL}/#{api_method}" }
+ let(:payload) { { foo: 'bar' } }
+
+ subject(:post) { described_class.new(slack_installation).post(api_method, payload) }
+
+ before do
+ stub_request(:post, api_url)
+ end
+
+ it 'posts to the Slack API correctly' do
+ post
+
+ expect(WebMock).to have_requested(:post, api_url).with(
+ body: payload.to_json,
+ headers: {
+ 'Authorization' => "Bearer #{slack_installation.bot_access_token}",
+ 'Content-Type' => 'application/json; charset=utf-8'
+ })
+ end
+
+ it 'returns the response' do
+ is_expected.to be_kind_of(HTTParty::Response)
+ end
+
+ context 'when the slack installation has no bot token' do
+ let(:slack_installation) { build(:slack_integration, :legacy) }
+
+ it 'raises an error' do
+ expect { post }.to raise_error(ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index f2c713c22a7..99326dfd02d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3743,7 +3743,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
[
{ key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: apple_app_store_integration.app_store_issuer_id, masked: true, public: false },
{ key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(apple_app_store_integration.app_store_private_key), masked: true, public: false },
- { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false }
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64', value: "true", masked: false, public: false }
]
end
@@ -3769,6 +3770,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil
end
end
end
@@ -3778,6 +3780,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil
end
end
end
diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb
index b2a52c8aaf0..7487793cf4f 100644
--- a/spec/models/integrations/apple_app_store_spec.rb
+++ b/spec/models/integrations/apple_app_store_spec.rb
@@ -81,6 +81,12 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
value: apple_app_store_integration.app_store_key_id,
masked: true,
public: false
+ },
+ {
+ key: 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64',
+ value: described_class::IS_KEY_CONTENT_BASE64,
+ masked: false,
+ public: false
}
]
diff --git a/spec/models/integrations/gitlab_slack_application_spec.rb b/spec/models/integrations/gitlab_slack_application_spec.rb
new file mode 100644
index 00000000000..68476dde2a3
--- /dev/null
+++ b/spec/models/integrations/gitlab_slack_application_spec.rb
@@ -0,0 +1,337 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::GitlabSlackApplication, feature_category: :integrations do
+ include AfterNextHelpers
+
+ it_behaves_like Integrations::BaseSlackNotification, factory: :gitlab_slack_application_integration do
+ before do
+ stub_request(:post, "#{::Slack::API::BASE_URL}/chat.postMessage").to_return(body: '{"ok":true}')
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.not_to validate_presence_of(:webhook) }
+ end
+
+ describe 'default values' do
+ it { expect(subject.category).to eq(:chat) }
+
+ it { is_expected.not_to be_alert_events }
+ it { is_expected.not_to be_commit_events }
+ it { is_expected.not_to be_confidential_issues_events }
+ it { is_expected.not_to be_confidential_note_events }
+ it { is_expected.not_to be_deployment_events }
+ it { is_expected.not_to be_issues_events }
+ it { is_expected.not_to be_job_events }
+ it { is_expected.not_to be_merge_requests_events }
+ it { is_expected.not_to be_note_events }
+ it { is_expected.not_to be_pipeline_events }
+ it { is_expected.not_to be_push_events }
+ it { is_expected.not_to be_tag_push_events }
+ it { is_expected.not_to be_vulnerability_events }
+ it { is_expected.not_to be_wiki_page_events }
+ end
+
+ describe '#execute' do
+ let_it_be(:user) { build_stubbed(:user) }
+
+ let(:slack_integration) { build(:slack_integration) }
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(integration.project, user) }
+ let(:slack_api_method_uri) { "#{::Slack::API::BASE_URL}/chat.postMessage" }
+
+ let(:mock_message) do
+ instance_double(Integrations::ChatMessage::PushMessage, attachments: ['foo'], pretext: 'bar')
+ end
+
+ subject(:integration) { build(:gitlab_slack_application_integration, slack_integration: slack_integration) }
+
+ before do
+ allow(integration).to receive(:get_message).and_return(mock_message)
+ allow(integration).to receive(:log_usage)
+ end
+
+ def stub_slack_request(channel: '#push_channel', success: true)
+ post_body = {
+ body: {
+ attachments: mock_message.attachments,
+ text: mock_message.pretext,
+ unfurl_links: false,
+ unfurl_media: false,
+ channel: channel
+ }
+ }
+
+ response = { ok: success }.to_json
+
+ stub_request(:post, slack_api_method_uri).with(post_body)
+ .to_return(body: response, headers: { 'Content-Type' => 'application/json; charset=utf-8' })
+ end
+
+ it 'notifies Slack' do
+ stub_slack_request
+
+ expect(integration.execute(data)).to be true
+ end
+
+ context 'when the integration is not configured for event' do
+ before do
+ integration.push_channel = nil
+ end
+
+ it 'does not notify Slack' do
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when Slack API responds with an error' do
+ it 'logs the error and API response' do
+ stub_slack_request(success: false)
+
+ expect(Gitlab::IntegrationsLogger).to receive(:error).with(
+ {
+ integration_class: described_class.name,
+ integration_id: integration.id,
+ project_id: integration.project_id,
+ project_path: kind_of(String),
+ message: 'Slack API error when notifying',
+ api_response: { 'ok' => false }
+ }
+ )
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when there is an HTTP error' do
+ it 'logs the error' do
+ expect_next(Slack::API).to receive(:post).and_raise(Net::ReadTimeout)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(
+ kind_of(Net::ReadTimeout),
+ {
+ slack_integration_id: slack_integration.id,
+ integration_id: integration.id
+ }
+ )
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when configured to post to multiple Slack channels' do
+ before do
+ push_channels = '#first_channel, #second_channel'
+ integration.push_channel = push_channels
+ end
+
+ it 'posts to both Slack channels and returns true' do
+ stub_slack_request(channel: '#first_channel')
+ stub_slack_request(channel: '#second_channel')
+
+ expect(integration.execute(data)).to be true
+ end
+
+ context 'when one of the posts responds with an error' do
+ it 'posts to both channels and returns true' do
+ stub_slack_request(channel: '#first_channel', success: false)
+ stub_slack_request(channel: '#second_channel')
+
+ expect(Gitlab::IntegrationsLogger).to receive(:error).once
+ expect(integration.execute(data)).to be true
+ end
+ end
+
+ context 'when both of the posts respond with an error' do
+ it 'posts to both channels and returns false' do
+ stub_slack_request(channel: '#first_channel', success: false)
+ stub_slack_request(channel: '#second_channel', success: false)
+
+ expect(Gitlab::IntegrationsLogger).to receive(:error).twice
+ expect(integration.execute(data)).to be false
+ end
+ end
+
+ context 'when one of the posts raises an HTTP exception' do
+ it 'posts to one channel and returns true' do
+ stub_slack_request(channel: '#second_channel')
+
+ expect_next_instance_of(Slack::API) do |api_client|
+ expect(api_client).to receive(:post)
+ .with('chat.postMessage', hash_including(channel: '#first_channel')).and_raise(Net::ReadTimeout)
+ expect(api_client).to receive(:post)
+ .with('chat.postMessage', hash_including(channel: '#second_channel')).and_call_original
+ end
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).once
+ expect(integration.execute(data)).to be true
+ end
+ end
+
+ context 'when both of the posts raise an HTTP exception' do
+ it 'posts to one channel and returns true' do
+ stub_slack_request(channel: '#second_channel')
+
+ expect_next(Slack::API).to receive(:post).twice.and_raise(Net::ReadTimeout)
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).twice
+ expect(integration.execute(data)).to be false
+ end
+ end
+ end
+ end
+
+ describe '#test' do
+ let(:integration) { build(:gitlab_slack_application_integration) }
+
+ let(:slack_api_method_uri) { "#{::Slack::API::BASE_URL}/chat.postEphemeral" }
+ let(:response_failure) { { error: 'channel_not_found' } }
+ let(:response_success) { { error: 'user_not_in_channel' } }
+ let(:response_headers) { { 'Content-Type' => 'application/json; charset=utf-8' } }
+ let(:request_body) do
+ {
+ text: 'Test',
+ user: integration.bot_user_id
+ }
+ end
+
+ subject(:result) { integration.test({}) }
+
+ def stub_slack_request(channel:, success:)
+ response_body = success ? response_success : response_failure
+
+ stub_request(:post, slack_api_method_uri)
+ .with(body: request_body.merge(channel: channel))
+ .to_return(body: response_body.to_json, headers: response_headers)
+ end
+
+ context 'when all channels can be posted to' do
+ before do
+ stub_slack_request(channel: anything, success: true)
+ end
+
+ it 'is successful' do
+ is_expected.to eq({ success: true, result: nil })
+ end
+ end
+
+ context 'when the same channel is used for multiple events' do
+ let(:integration) do
+ build(:gitlab_slack_application_integration, all_channels: false, push_channel: '#foo', issue_channel: '#foo')
+ end
+
+ it 'only tests the channel once' do
+ stub_slack_request(channel: '#foo', success: true)
+
+ is_expected.to eq({ success: true, result: nil })
+ expect(WebMock).to have_requested(:post, slack_api_method_uri).once
+ end
+ end
+
+ context 'when there are channels that cannot be posted to' do
+ let(:unpostable_channels) { ['#push_channel', '#issue_channel'] }
+
+ before do
+ stub_slack_request(channel: anything, success: true)
+
+ unpostable_channels.each do |channel|
+ stub_slack_request(channel: channel, success: false)
+ end
+ end
+
+ it 'returns an error message informing which channels cannot be posted to' do
+ expected_message = "Unable to post to #{unpostable_channels.to_sentence}, " \
+ 'please add the GitLab Slack app to any private Slack channels'
+
+ is_expected.to eq({ success: false, result: expected_message })
+ end
+
+ context 'when integration is not configured for notifications' do
+ let_it_be(:integration) { build(:gitlab_slack_application_integration, all_channels: false) }
+
+ it 'is successful' do
+ is_expected.to eq({ success: true, result: nil })
+ end
+ end
+ end
+
+ context 'when integration is using legacy version of Slack app' do
+ before do
+ integration.slack_integration = build(:slack_integration, :legacy)
+ end
+
+ it 'returns an error to inform the user to update their integration' do
+ expected_message = 'GitLab for Slack app must be reinstalled to enable notifications'
+
+ is_expected.to eq({ success: false, result: expected_message })
+ end
+ end
+ end
+
+ context 'when the integration is active' do
+ before do
+ subject.active = true
+ end
+
+ it 'is editable, and presents editable fields' do
+ expect(subject).to be_editable
+ expect(subject.fields).not_to be_empty
+ expect(subject.configurable_events).not_to be_empty
+ end
+
+ it 'includes the expected sections' do
+ section_types = subject.sections.pluck(:type)
+
+ expect(section_types).to eq(
+ [
+ described_class::SECTION_TYPE_TRIGGER,
+ described_class::SECTION_TYPE_CONFIGURATION
+ ]
+ )
+ end
+ end
+
+ context 'when the integration is not active' do
+ before do
+ subject.active = false
+ end
+
+ it 'is not editable, and presents no editable fields' do
+ expect(subject).not_to be_editable
+ expect(subject.fields).to be_empty
+ expect(subject.configurable_events).to be_empty
+ end
+
+ it 'does not include sections' do
+ section_types = subject.sections.pluck(:type)
+
+ expect(section_types).to be_empty
+ end
+ end
+
+ describe '#description' do
+ specify { expect(subject.description).to be_present }
+ end
+
+ describe '#upgrade_needed?' do
+ context 'with all_features_supported' do
+ subject(:integration) { create(:gitlab_slack_application_integration, :all_features_supported) }
+
+ it 'is false' do
+ expect(integration).not_to be_upgrade_needed
+ end
+ end
+
+ context 'without all_features_supported' do
+ subject(:integration) { create(:gitlab_slack_application_integration) }
+
+ it 'is true' do
+ expect(integration).to be_upgrade_needed
+ end
+ end
+
+ context 'without slack_integration' do
+ subject(:integration) { create(:gitlab_slack_application_integration, slack_integration: nil) }
+
+ it 'is false' do
+ expect(integration).not_to be_upgrade_needed
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/slack_workspace/api_scope_spec.rb b/spec/models/integrations/slack_workspace/api_scope_spec.rb
new file mode 100644
index 00000000000..92052983242
--- /dev/null
+++ b/spec/models/integrations/slack_workspace/api_scope_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackWorkspace::ApiScope, feature_category: :integrations do
+ describe '.find_or_initialize_by_names' do
+ it 'acts as insert into a global set of scope names' do
+ expect { described_class.find_or_initialize_by_names(%w[foo bar baz]) }
+ .to change { described_class.count }.by(3)
+
+ expect { described_class.find_or_initialize_by_names(%w[bar baz foo buzz]) }
+ .to change { described_class.count }.by(1)
+
+ expect { described_class.find_or_initialize_by_names(%w[baz foo]) }
+ .to change { described_class.count }.by(0)
+
+ expect(described_class.pluck(:name)).to match_array(%w[foo bar baz buzz])
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f3aa174a964..a66302da6f5 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -4082,12 +4082,18 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
)
end
- context 'when service class is Ci::CompareCodequalityReportsService' do
- let(:service_class) { 'Ci::CompareCodequalityReportsService' }
+ context 'when service class uses merge base pipeline' do
+ where(:service_class) do
+ %w[
+ Ci::CompareMetricsReportsService
+ Ci::CompareCodequalityReportsService
+ Ci::CompareSecurityReportsService
+ ]
+ end
context 'when merge request has a merge request pipeline' do
let(:merge_request) do
- create(:merge_request, :with_merge_request_pipeline)
+ create(:merge_request, :with_merge_request_pipeline, source_project: project)
end
let(:merge_base_pipeline) do
@@ -4099,8 +4105,36 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
merge_request.update_head_pipeline
end
- it 'returns the merge_base_pipeline' do
- expect(pipeline).to eq(merge_base_pipeline)
+ with_them do
+ it 'returns the merge_base_pipeline' do
+ expect(pipeline).to eq(merge_base_pipeline)
+ end
+ end
+ end
+
+ context 'when merge does not have a merge request pipeline' do
+ with_them do
+ it 'returns the base_pipeline' do
+ expect(pipeline).to eq(base_pipeline)
+ end
+ end
+ end
+ end
+
+ context 'when service class is Ci::CompareSecurityReportsService and feature flag is off' do
+ let(:service_class) { 'Ci::CompareSecurityReportsService' }
+
+ before do
+ stub_feature_flags(use_merge_base_for_security_widget: false)
+ end
+
+ context 'when merge request has a merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request, :with_merge_request_pipeline, source_project: project)
+ end
+
+ it 'returns the base pipeline' do
+ expect(pipeline).to eq(base_pipeline)
end
end
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index 80351862fc1..d237a16da8f 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -18,24 +18,6 @@ RSpec.describe ResourceMilestoneEvent, feature_category: :team_planning, type: :
it { is_expected.to belong_to(:milestone) }
end
- describe 'scopes' do
- describe '.aliased_for_timebox_report', :freeze_time do
- let!(:event) { create(:resource_milestone_event, milestone: milestone) }
-
- let(:milestone) { create(:milestone) }
- let(:scope) { described_class.aliased_for_timebox_report.first }
-
- it 'returns correct values with aliased names', :aggregate_failures do
- expect(scope.event_type).to eq('timebox')
- expect(scope.id).to eq(event.id)
- expect(scope.issue_id).to eq(event.issue_id)
- expect(scope.value).to eq(milestone.id)
- expect(scope.action).to eq(event.action)
- expect(scope.created_at).to eq(event.created_at)
- end
- end
- end
-
describe '#milestone_title' do
let(:milestone) { create(:milestone, title: 'v2.3') }
let(:event) { create(:resource_milestone_event, milestone: milestone) }
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
index 699720b564a..a6d6b507b69 100644
--- a/spec/models/resource_state_event_spec.rb
+++ b/spec/models/resource_state_event_spec.rb
@@ -41,23 +41,6 @@ RSpec.describe ResourceStateEvent, feature_category: :team_planning, type: :mode
end
end
- describe 'scopes' do
- describe '.aliased_for_timebox_report', :freeze_time do
- let!(:event) { create(:resource_state_event, issue: issue) }
-
- let(:scope) { described_class.aliased_for_timebox_report.first }
-
- it 'returns correct values with aliased names', :aggregate_failures do
- expect(scope.event_type).to eq('state')
- expect(scope.id).to eq(event.id)
- expect(scope.issue_id).to eq(event.issue_id)
- expect(scope.value).to eq(issue.state_id)
- expect(scope.action).to eq(nil)
- expect(scope.created_at).to eq(event.created_at)
- end
- end
- end
-
context 'callbacks' do
describe '#issue_usage_metrics' do
describe 'when an issue is closed' do
diff --git a/spec/models/slack_integration_spec.rb b/spec/models/slack_integration_spec.rb
new file mode 100644
index 00000000000..41beeee598c
--- /dev/null
+++ b/spec/models/slack_integration_spec.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe SlackIntegration, feature_category: :integrations do
+ describe "Associations" do
+ it { is_expected.to belong_to(:integration) }
+ end
+
+ describe 'authorized_scope_names' do
+ subject(:slack_integration) { create(:slack_integration) }
+
+ it 'accepts assignment to nil' do
+ slack_integration.update!(authorized_scope_names: nil)
+
+ expect(slack_integration.authorized_scope_names).to be_empty
+ end
+
+ it 'accepts assignment to a string' do
+ slack_integration.update!(authorized_scope_names: 'foo')
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo')
+ end
+
+ it 'accepts assignment to an array of strings' do
+ slack_integration.update!(authorized_scope_names: %w[foo bar])
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar')
+ end
+
+ it 'accepts assignment to a comma-separated string' do
+ slack_integration.update!(authorized_scope_names: 'foo,bar')
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar')
+ end
+
+ it 'strips white-space' do
+ slack_integration.update!(authorized_scope_names: 'foo , bar,baz')
+
+ expect(slack_integration.authorized_scope_names).to contain_exactly('foo', 'bar', 'baz')
+ end
+ end
+
+ describe 'all_features_supported?/upgrade_needed?' do
+ subject(:slack_integration) { create(:slack_integration) }
+
+ context 'with enough scopes' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[chat:write.public chat:write commands])
+ end
+
+ it { is_expected.to be_all_features_supported }
+ it { is_expected.not_to be_upgrade_needed }
+ end
+
+ %w[chat:write.public chat:write].each do |scope_name|
+ context "without #{scope_name}" do
+ before do
+ scopes = %w[chat:write.public chat:write] - [scope_name]
+ slack_integration.update!(authorized_scope_names: scopes)
+ end
+
+ it { is_expected.not_to be_all_features_supported }
+ it { is_expected.to be_upgrade_needed }
+ end
+ end
+ end
+
+ describe 'feature_available?' do
+ subject(:slack_integration) { create(:slack_integration) }
+
+ context 'without any scopes' do
+ it 'is always true for :commands' do
+ expect(slack_integration).to be_feature_available(:commands)
+ end
+
+ it 'is always false for others' do
+ expect(slack_integration).not_to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+
+ context 'with enough scopes for notifications' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[chat:write.public chat:write foo])
+ end
+
+ it 'only has the correct features' do
+ expect(slack_integration).to be_feature_available(:commands)
+ expect(slack_integration).to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+
+ context 'with enough scopes for commands' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[commands foo])
+ end
+
+ it 'only has the correct features' do
+ expect(slack_integration).to be_feature_available(:commands)
+ expect(slack_integration).not_to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+
+ context 'with all scopes' do
+ before do
+ slack_integration.update!(authorized_scope_names: %w[commands chat:write chat:write.public])
+ end
+
+ it 'only has the correct features' do
+ expect(slack_integration).to be_feature_available(:commands)
+ expect(slack_integration).to be_feature_available(:notifications)
+ expect(slack_integration).not_to be_feature_available(:foo)
+ end
+ end
+ end
+
+ describe 'Scopes' do
+ let_it_be(:slack_integration) { create(:slack_integration) }
+ let_it_be(:legacy_slack_integration) { create(:slack_integration, :legacy) }
+
+ describe '#with_bot' do
+ it 'returns records with bot data' do
+ expect(described_class.with_bot).to contain_exactly(slack_integration)
+ end
+ end
+
+ describe '#by_team' do
+ it 'returns records with shared team_id' do
+ team_id = slack_integration.team_id
+ team_slack_integration = create(:slack_integration, team_id: team_id)
+
+ expect(described_class.by_team(team_id)).to contain_exactly(slack_integration, team_slack_integration)
+ end
+ end
+ end
+
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:team_id) }
+ it { is_expected.to validate_presence_of(:team_name) }
+ it { is_expected.to validate_presence_of(:alias) }
+ it { is_expected.to validate_presence_of(:user_id) }
+ it { is_expected.to validate_presence_of(:integration) }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bc677aca0f4..b36599b1273 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -105,9 +105,6 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to delegate_method(:registration_objective).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:registration_objective=).to(:user_detail).with_arguments(:args).allow_nil }
- it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
- it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
-
it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil }
diff --git a/spec/requests/api/users_preferences_spec.rb b/spec/requests/api/users_preferences_spec.rb
index ef9735fd8b0..067acd150f3 100644
--- a/spec/requests/api/users_preferences_spec.rb
+++ b/spec/requests/api/users_preferences_spec.rb
@@ -10,17 +10,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns a success status and the value has been changed' do
put api("/user/preferences", user), params: {
view_diffs_file_by_file: true,
- show_whitespace_in_diffs: true
+ show_whitespace_in_diffs: true,
+ pass_user_identities_to_ci_jwt: true
}
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['view_diffs_file_by_file']).to eq(true)
expect(json_response['show_whitespace_in_diffs']).to eq(true)
+ expect(json_response['pass_user_identities_to_ci_jwt']).to eq(true)
user.reload
expect(user.view_diffs_file_by_file).to be_truthy
expect(user.show_whitespace_in_diffs).to be_truthy
+ expect(user.pass_user_identities_to_ci_jwt).to be_truthy
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 65b1eb9dfa4..8f55ee705ab 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -357,24 +357,26 @@ RSpec.describe Projects::UpdateService, feature_category: :projects do
# Using some sample features for testing.
# Not using all the features because some of them must be enabled/disabled together
%w[issues wiki forking].each do |feature_name|
- let(:feature) { "#{feature_name}_access_level" }
- let(:params) do
- { project_feature_attributes: { feature => ProjectFeature::ENABLED } }
- end
+ context "with feature_name:#{feature_name}" do
+ let(:feature) { "#{feature_name}_access_level" }
+ let(:params) do
+ { project_feature_attributes: { feature => ProjectFeature::ENABLED } }
+ end
- before do
- project.project_feature.update!(feature => ProjectFeature::DISABLED)
- end
+ before do
+ project.project_feature.update!(feature => ProjectFeature::DISABLED)
+ end
- it 'publishes Projects::ProjectFeaturesChangedEvent' do
- expect { update_project(project, user, params) }
- .to publish_event(Projects::ProjectFeaturesChangedEvent)
- .with(
- project_id: project.id,
- namespace_id: project.namespace_id,
- root_namespace_id: project.root_namespace.id,
- features: ["updated_at", feature]
- )
+ it 'publishes Projects::ProjectFeaturesChangedEvent' do
+ expect { update_project(project, user, params) }
+ .to publish_event(Projects::ProjectFeaturesChangedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id,
+ features: array_including(feature, "updated_at")
+ )
+ end
end
end
end
diff --git a/spec/services/user_preferences/update_service_spec.rb b/spec/services/user_preferences/update_service_spec.rb
index 09acc01729e..601a023c30d 100644
--- a/spec/services/user_preferences/update_service_spec.rb
+++ b/spec/services/user_preferences/update_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe UserPreferences::UpdateService, feature_category: :user_profile do
let(:user) { create(:user) }
- let(:params) { { view_diffs_file_by_file: false } }
+ let(:params) { { view_diffs_file_by_file: false, pass_user_identities_to_ci_jwt: true } }
describe '#execute' do
subject(:service) { described_class.new(user, params) }
@@ -15,6 +15,8 @@ RSpec.describe UserPreferences::UpdateService, feature_category: :user_profile d
expect(result.status).to eq(:success)
expect(result.payload[:preferences].view_diffs_file_by_file).to eq(params[:view_diffs_file_by_file])
+ expect(result.payload[:preferences].pass_user_identities_to_ci_jwt
+ ).to eq(params[:pass_user_identities_to_ci_jwt])
end
end
diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb
index 37aa5fed838..ebd2502398d 100644
--- a/spec/services/users/upsert_credit_card_validation_service_spec.rb
+++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user_profile do
- let_it_be(:user) { create(:user, requires_credit_card_verification: true) }
+ let_it_be(:user) { create(:user) }
let(:user_id) { user.id }
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
@@ -21,7 +21,7 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
end
describe '#execute' do
- subject(:service) { described_class.new(params, user) }
+ subject(:service) { described_class.new(params) }
context 'successfully set credit card validation record for the user' do
context 'when user does not have credit card validation record' do
@@ -42,10 +42,6 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
expiration_date: Date.new(expiration_year, 1, 31)
)
end
-
- it 'sets the requires_credit_card_verification attribute on the user to false' do
- expect { service.execute }.to change { user.reload.requires_credit_card_verification }.to(false)
- end
end
context 'when user has credit card validation record' do
diff --git a/spec/support/matchers/snapshot_matcher.rb b/spec/support/matchers/snapshot_matcher.rb
new file mode 100644
index 00000000000..ec1e9cb0815
--- /dev/null
+++ b/spec/support/matchers/snapshot_matcher.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_snapshot do |date, expected_states|
+ match do |actual_snapshots|
+ snapshot = actual_snapshots.find { |snapshot| snapshot[:date] == date }
+
+ @snapshot_not_found = snapshot.nil?
+ @item_states_not_found = []
+ @not_eq_error = nil
+
+ break false if @snapshot_not_found
+
+ expected_states.each do |expected_state|
+ actual_state = snapshot[:item_states].find { |state| state[:item_id] == expected_state[:item_id] }
+
+ if actual_state.nil?
+ @item_states_not_found << expected_state[:issue_id]
+ else
+ default_state = {
+ weight: 0,
+ start_state: ResourceStateEvent.states[:opened],
+ end_state: ResourceStateEvent.states[:opened],
+ parent_id: nil,
+ children_ids: Set.new
+ }
+ begin
+ expect(actual_state).to eq(default_state.merge(expected_state))
+ rescue RSpec::Expectations::ExpectationNotMetError => e
+ @error_item_title = WorkItem.find(expected_state[:item_id]).title
+ @not_eq_error = e
+
+ raise
+ end
+ end
+ end
+ end
+
+ failure_message do |_|
+ break "No snapshot found for the given date #{date}" if @snapshot_not_found
+
+ messages = []
+
+ messages << <<~MESSAGE
+ Expected the snapshot on #{date} to match the expected snapshot.
+
+ Errors:
+ MESSAGE
+
+ messages << "Item states not found for: #{@item_states_not_found.join(', ')}" unless @item_states_not_found.empty?
+
+ messages << "`#{@error_item_title}` does not have the expected states.\n#{@not_eq_error}" if @not_eq_error
+
+ messages.join("\n")
+ end
+end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 6e377b8cb0d..94c43669173 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -50,7 +50,10 @@ RSpec.configure do |config|
# Add warning for example missing feature_category
config.before do |example|
if warn_missing_feature_category && example.metadata[:feature_category].blank? && !ENV['CI']
- warn "Missing metadata feature_category: #{example.location} See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata"
+ location =
+ example.metadata[:shared_group_inclusion_backtrace].last&.formatted_inclusion_location ||
+ example.location
+ warn "Missing metadata feature_category: #{location} See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata"
end
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 0426eefebbd..3581282857a 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -5138,7 +5138,6 @@
- './spec/helpers/members_helper_spec.rb'
- './spec/helpers/merge_requests_helper_spec.rb'
- './spec/helpers/namespaces_helper_spec.rb'
-- './spec/helpers/nav_helper_spec.rb'
- './spec/helpers/nav/new_dropdown_helper_spec.rb'
- './spec/helpers/nav/top_nav_helper_spec.rb'
- './spec/helpers/notes_helper_spec.rb'
diff --git a/tooling/lib/tooling/gettext_extractor.rb b/tooling/lib/tooling/gettext_extractor.rb
index 673749a5a16..2ec6183673c 100644
--- a/tooling/lib/tooling/gettext_extractor.rb
+++ b/tooling/lib/tooling/gettext_extractor.rb
@@ -20,7 +20,7 @@ module Tooling
end
def initialize(
- backend_glob: "{ee,app,lib,config,locale}/**/*.{rb,erb,haml}",
+ backend_glob: "{ee/,}{app,lib,config,locale}/**/*.{rb,erb,haml}",
glob_base: nil,
package_name: 'gitlab',
package_version: '1.0.0'