diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e276163848..6ef74f40d47 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,13 +42,16 @@ default: CREATE_RAILS_FLAKY_TEST_ISSUES: "true" CREATE_RAILS_SLOW_TEST_ISSUES: "true" CREATE_RAILS_TEST_FAILURE_ISSUES: "true" + GLCI_PUSH_RAILS_TEST_FAILURE_ISSUES_TO_GCS: "true" .default-merge-request-variables: &default-merge-request-variables - NO_SOURCEMAPS: "true" ADD_SLOW_TEST_NOTE_TO_MERGE_REQUEST: "true" + CREATE_RAILS_TEST_FAILURE_ISSUES: "false" FF_NETWORK_PER_BUILD: "true" FF_TIMESTAMPS: "true" FF_USE_FASTZIP: "true" + NO_SOURCEMAPS: "true" + GLCI_PUSH_RAILS_TEST_FAILURE_ISSUES_TO_GCS: "true" .if-merge-request-security-canonical-sync: &if-merge-request-security-canonical-sync if: '$CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == "gitlab-org/security/gitlab" && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $CI_DEFAULT_BRANCH && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH' diff --git a/.gitlab/ci/as-if-foss.gitlab-ci.yml b/.gitlab/ci/as-if-foss.gitlab-ci.yml index c3913a93c6f..237e9590650 100644 --- a/.gitlab/ci/as-if-foss.gitlab-ci.yml +++ b/.gitlab/ci/as-if-foss.gitlab-ci.yml @@ -70,6 +70,7 @@ start-as-if-foss: variables: - GITLAB_LARGE_RUNNER_OPTIONAL - GLCI_PRODUCTION_ASSETS_RUNNER_OPTIONAL + - GLCI_PUSH_RAILS_TEST_FAILURE_ISSUES_TO_GCS variables: START_AS_IF_FOSS: $START_AS_IF_FOSS RUBY_VERSION: $RUBY_VERSION diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml index c359d226946..7c27162e0fd 100644 --- a/.gitlab/ci/rails/shared.gitlab-ci.yml +++ b/.gitlab/ci/rails/shared.gitlab-ci.yml @@ -119,7 +119,7 @@ include: - bundle exec gem list gitlab_quality-test_tooling - | section_start "failed-test-issues" "Report test failures" - if [[ "$CREATE_RAILS_TEST_FAILURE_ISSUES" == "true" ]] && [[ -n "$TEST_FAILURES_PROJECT_TOKEN" ]]; then + if [[ -n "$TEST_FAILURES_PROJECT_TOKEN" ]]; then input_file="rspec/rspec-${CI_JOB_ID}.json" # The actual failures will always be part of the retry report @@ -127,14 +127,35 @@ include: input_file="rspec/rspec-retry-${CI_JOB_ID}.json" fi - bundle exec failed-test-issues \ - --token "${TEST_FAILURES_PROJECT_TOKEN}" \ - --project "gitlab-org/gitlab" \ - --input-files "${input_file}" \ - --exclude-labels-for-search "QA,knapsack_report" \ - --related-issues-file "rspec/${CI_JOB_ID}-failed-test-issues.json"; + cmd_args=() + cmd_args+=(--token "${TEST_FAILURES_PROJECT_TOKEN}") + cmd_args+=(--project "gitlab-org/gitlab") + cmd_args+=(--input-files "${input_file}") + cmd_args+=(--exclude-labels-for-search "QA,knapsack_report") + cmd_args+=(--related-issues-file "rspec/${CI_JOB_ID}-failed-test-issues.json") + + if [[ "$CREATE_RAILS_TEST_FAILURE_ISSUES" != "true" ]]; then + cmd_args+=(--enable-issue-update false) + echoinfo "Disabling issue creation because \$CREATE_RAILS_TEST_FAILURE_ISSUES != 'true'" + fi + + if [[ "$GLCI_PUSH_RAILS_TEST_FAILURE_ISSUES_TO_GCS" == "true" ]]; then + if [[ -n "$GLCI_FAILED_TESTS_GCS_PROJECT_ID" && -n "$GLCI_FAILED_TESTS_GCS_BUCKET" && -n "$GLCI_FAILED_TESTS_GCS_CREDENTIALS_FILE" ]]; then + cmd_args+=(--enable-gcs true) + cmd_args+=(--gcs-project-id "$GLCI_FAILED_TESTS_GCS_PROJECT_ID") + cmd_args+=(--gcs-bucket "$GLCI_FAILED_TESTS_GCS_BUCKET") + cmd_args+=(--gcs-credentials "$GLCI_FAILED_TESTS_GCS_CREDENTIALS_FILE") + else + echoerr "Skipping GCS push because one or more GLCI_FAILED_TESTS_* environment variables are not set" + fi + else + echoinfo "Skipping GCS push because \$GLCI_PUSH_RAILS_TEST_FAILURE_ISSUES_TO_GCS != 'true'" + fi + + # Execute command with all arguments + bundle exec failed-test-issues "${cmd_args[@]}" else - echoinfo "Not reporting test failures because \$CREATE_RAILS_TEST_FAILURE_ISSUES != 'true' or TEST_FAILURES_PROJECT_TOKEN is not set" + echoinfo "Not reporting test failures because TEST_FAILURES_PROJECT_TOKEN is not set" fi section_end "failed-test-issues" - | diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 29bef23974f..6cac2f23468 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -2678,7 +2678,6 @@ Layout/LineLength: - 'spec/features/projects/show/user_sees_deletion_failure_message_spec.rb' - 'spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb' - 'spec/features/projects/terraform_spec.rb' - - 'spec/features/projects/tree/upload_file_spec.rb' - 'spec/features/projects_spec.rb' - 'spec/features/search/user_searches_for_comments_spec.rb' - 'spec/features/search/user_searches_for_merge_requests_spec.rb' diff --git a/.rubocop_todo/rails/file_path.yml b/.rubocop_todo/rails/file_path.yml index 3026e4efe73..b2433868daf 100644 --- a/.rubocop_todo/rails/file_path.yml +++ b/.rubocop_todo/rails/file_path.yml @@ -80,7 +80,6 @@ Rails/FilePath: - 'spec/features/projects/settings/repository_settings_spec.rb' - 'spec/features/projects/settings/user_changes_avatar_spec.rb' - 'spec/features/projects/snippets/create_snippet_spec.rb' - - 'spec/features/projects/tree/upload_file_spec.rb' - 'spec/features/snippets/user_creates_snippet_spec.rb' - 'spec/features/snippets/user_edits_snippet_spec.rb' - 'spec/features/uploads/user_uploads_avatar_to_group_spec.rb' diff --git a/.rubocop_todo/rspec/avoid_conditional_statements.yml b/.rubocop_todo/rspec/avoid_conditional_statements.yml index ef65040453f..fc589a52ac5 100644 --- a/.rubocop_todo/rspec/avoid_conditional_statements.yml +++ b/.rubocop_todo/rspec/avoid_conditional_statements.yml @@ -62,8 +62,6 @@ RSpec/AvoidConditionalStatements: - 'spec/features/projects/settings/repository_settings_spec.rb' - 'spec/features/projects/settings/user_transfers_a_project_spec.rb' - 'spec/features/projects/show/user_sees_git_instructions_spec.rb' - - 'spec/features/projects/tree/create_directory_spec.rb' - - 'spec/features/projects/tree/create_file_spec.rb' - 'spec/features/projects_spec.rb' - 'spec/features/search/user_uses_header_search_field_spec.rb' - 'spec/features/usage_stats_consent_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index c01573433a4..c4c8cc06f49 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -102,7 +102,6 @@ RSpec/ContextWording: - 'ee/spec/features/groups/saml_providers_spec.rb' - 'ee/spec/features/groups/security/compliance_dashboards_spec.rb' - 'ee/spec/features/groups_spec.rb' - - 'ee/spec/features/ide/user_opens_ide_spec.rb' - 'ee/spec/features/issues/epic_in_issue_sidebar_spec.rb' - 'ee/spec/features/issues/filtered_search/filter_issues_by_iteration_spec.rb' - 'ee/spec/features/issues/form_spec.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index bd3be4cc017..6ccb6357d17 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -675,8 +675,6 @@ Style/IfUnlessModifier: - 'spec/factories/users.rb' - 'spec/features/merge_request/batch_comments_spec.rb' - 'spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb' - - 'spec/features/projects/tree/create_directory_spec.rb' - - 'spec/features/projects/tree/create_file_spec.rb' - 'spec/graphql/mutations/releases/update_spec.rb' - 'spec/lib/container_registry/gitlab_api_client_spec.rb' - 'spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb' diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 4b856550f2b..5d126a2e000 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -2,8 +2,6 @@ import Vue from 'vue'; import { IDE_ELEMENT_ID } from '~/ide/constants'; import PerformancePlugin from '~/performance/vue_performance_plugin'; import Translate from '~/vue_shared/translate'; -import { parseBoolean } from '../lib/utils/common_utils'; -import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; import { OAuthCallbackDomainMismatchErrorApp } from './oauth_callback_domain_mismatch_error'; Vue.use(Translate); @@ -17,22 +15,13 @@ Vue.use(PerformancePlugin, { * * @param {Objects} options - Extra options for the IDE (Used by EE). */ -export async function startIde(options) { +export async function startIde() { const ideElement = document.getElementById(IDE_ELEMENT_ID); if (!ideElement) { return; } - const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde); - - if (!useNewWebIde) { - resetServiceWorkersPublicPath(); - const { initLegacyWebIDE } = await import('./init_legacy_web_ide'); - initLegacyWebIDE(ideElement, options); - return; - } - const oAuthCallbackDomainMismatchApp = new OAuthCallbackDomainMismatchErrorApp(ideElement); if (oAuthCallbackDomainMismatchApp.shouldRenderError()) { diff --git a/app/assets/javascripts/work_items/components/create_work_item.vue b/app/assets/javascripts/work_items/components/create_work_item.vue index adc9cfbba60..88bc513cd81 100644 --- a/app/assets/javascripts/work_items/components/create_work_item.vue +++ b/app/assets/javascripts/work_items/components/create_work_item.vue @@ -520,7 +520,7 @@ export default { Boolean(this.workItemDueDateIsFixed) || Boolean(this.workItemStartDateIsFixed) || Boolean(this.workItemIterationId) || - (this.glFeatures.customFieldsFeature && isCustomFieldsFilled) + isCustomFieldsFilled ); }, shouldDatesRollup() { @@ -529,9 +529,6 @@ export default { workItemCustomFields() { return findWidget(WIDGET_TYPE_CUSTOM_FIELDS, this.workItem)?.customFieldValues ?? null; }, - showWorkItemCustomFields() { - return this.glFeatures.customFieldsFeature && this.workItemCustomFields; - }, }, watch: { shouldDiscardDraft: { @@ -1049,7 +1046,7 @@ export default { @error="$emit('error', $event)" /> " @@ -24,7 +27,7 @@ module RapidDiffs rescue StandardError => e Gitlab::AppLogger.error("Error streaming diffs: #{e.message}") error_component = ::RapidDiffs::StreamingErrorComponent.new(message: e.message) - response.stream.write error_component.render_in(view_context) + response.stream.write error_component.render_in(context) ensure response.stream.close end @@ -55,7 +58,7 @@ module RapidDiffs helpers.diff_view end - def stream_diff_files(options) + def stream_diff_files(options, view_context) return unless resource diffs = resource.diffs_for_streaming(options) @@ -68,26 +71,40 @@ module RapidDiffs # NOTE: This is a temporary flag to test out the new diff_blobs if !!ActiveModel::Type::Boolean.new.cast(params.permit(:diff_blobs)[:diff_blobs]) - stream_diff_blobs(options) + stream_diff_blobs(options, view_context) else - diffs.diff_files.each do |diff_file| - response.stream.write(render_diff_file(diff_file)) - end + stream_diff_collection(diffs.diff_files, view_context) end end - def render_diff_file(diff_file) - render_to_string( - ::RapidDiffs::DiffFileComponent.new(diff_file: diff_file, parallel_view: view == :parallel), - layout: false - ) + def stream_diff_collection(diff_files, view_context) + each_growing_slice(diff_files, 5, 2) do |slice| + response.stream.write(render_diff_files_collection(slice, view_context)) + end end - def stream_diff_blobs(options) + def each_growing_slice(collection, initial_size, growth_factor = 2) + position = 0 + size = initial_size + total = collection.count + + while position < total + end_pos = [position + size, total].min + yield collection.to_a[position...end_pos] if block_given? + + position = end_pos + size = (size * growth_factor).to_i + end + end + + def render_diff_files_collection(diff_files, view_context) + ::RapidDiffs::DiffFileComponent.with_collection(diff_files, parallel_view: view == :parallel) + .render_in(view_context) + end + + def stream_diff_blobs(options, view_context) resource.diffs_for_streaming(options) do |diff_files_batch| - diff_files_batch.each do |diff_file| - response.stream.write(render_diff_file(diff_file)) - end + response.stream.write(render_diff_files_collection(diff_files_batch, view_context)) end end diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index a84903fd6ef..41133caa210 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -26,11 +26,10 @@ class IdeController < ApplicationController @fork_info = fork_info(project, params[:branch]) push_frontend_feature_flag(:web_ide_multi_domain, @project.group) - render layout: helpers.use_new_web_ide? ? 'fullscreen' : 'application' + render layout: 'fullscreen' end def oauth_redirect - return render_404 unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) # TODO - It's **possible** we end up here and no oauth application has been set up. # We need to have better handling of these edge cases. Here's a follow-up issue: # https://gitlab.com/gitlab-org/gitlab/-/issues/433322 @@ -54,8 +53,6 @@ class IdeController < ApplicationController end def ensure_web_ide_oauth_application! - return unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) - ::WebIde::DefaultOauthApplication.ensure_oauth_application! end diff --git a/app/controllers/projects/merge_requests/diffs_stream_controller.rb b/app/controllers/projects/merge_requests/diffs_stream_controller.rb index 1e03f1edaf3..b1b98aa82e0 100644 --- a/app/controllers/projects/merge_requests/diffs_stream_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_stream_controller.rb @@ -11,15 +11,10 @@ module Projects @merge_request end - def render_diff_file(diff_file) - render_to_string( - ::RapidDiffs::MergeRequestDiffFileComponent.new( - diff_file: diff_file, - merge_request: @merge_request, - parallel_view: view == :parallel - ), - layout: false - ) + def render_diff_files_collection(diff_files, view_context) + ::RapidDiffs::MergeRequestDiffFileComponent + .with_collection(diff_files, merge_request: @merge_request, parallel_view: view == :parallel) + .render_in(view_context) end end end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 44d1879bed1..743c315be83 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -21,6 +21,7 @@ # Both parent and include_parent_descendants params must be present. # include_ancestors: boolean (defaults to true) # organization: Scope the groups to the Organizations::Organization +# active: boolean - filters for active groups. # # Users with full private access can see all groups. The `owned` and `parent` # params can be used to restrict the groups that are returned. @@ -103,6 +104,7 @@ class GroupsFinder < UnionFinder def filter_groups(groups) groups = by_organization(groups) + groups = by_active(groups) groups = by_parent(groups) groups = by_custom_attributes(groups) groups = filter_group_ids(groups) @@ -161,6 +163,12 @@ class GroupsFinder < UnionFinder groups.id_not_in(params[:exclude_group_ids]) end + def by_active(groups) + return groups if params[:active].nil? + + params[:active] ? groups.active : groups.inactive + end + def include_parent_shared_groups? params.fetch(:include_parent_shared_groups, false) end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 8c7a24b2ce2..6e3408a0376 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -4,12 +4,11 @@ module IdeHelper # Overridden in EE def ide_data(project:, fork_info:, params:) base_data = { - 'use-new-web-ide' => use_new_web_ide?.to_s, 'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/_index.md'), 'sign-in-path' => new_session_path(current_user), 'sign-out-path' => destroy_user_session_path, 'user-preferences-path' => profile_preferences_path - }.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project)) + }.merge(new_ide_data(project: project)) return base_data unless project @@ -22,8 +21,6 @@ module IdeHelper end def show_web_ide_oauth_callback_mismatch_callout? - return false unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) - callback_urls = ::WebIde::DefaultOauthApplication.oauth_application_callback_urls callback_url_domains = callback_urls.map { |url| URI.parse(url).origin } callback_url_domains.any? && callback_url_domains.exclude?(request.base_url) @@ -33,10 +30,6 @@ module IdeHelper ::WebIde::DefaultOauthApplication.oauth_application_id end - def use_new_web_ide? - Feature.enabled?(:vscode_web_ide, current_user) - end - private def new_ide_fonts @@ -66,7 +59,6 @@ module IdeHelper end def new_ide_oauth_data - return {} unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) return {} unless ::WebIde::DefaultOauthApplication.oauth_application client_id = ::WebIde::DefaultOauthApplication.oauth_application.uid @@ -95,32 +87,6 @@ module IdeHelper }.merge(new_ide_code_suggestions_data).merge(new_ide_oauth_data) end - def legacy_ide_data(project:) - { - 'empty-state-svg-path' => image_path('illustrations/empty-state/empty-variables-md.svg'), - 'no-changes-state-svg-path' => image_path('illustrations/status/status-nothing-sm.svg'), - 'committed-state-svg-path' => image_path('illustrations/rocket-launch-md.svg'), - 'pipelines-empty-state-svg-path': image_path('illustrations/empty-state/empty-pipeline-md.svg'), - 'switch-editor-svg-path': image_path('illustrations/rocket-launch-md.svg'), - 'ci-help-page-path' => help_page_path('ci/quick_start/_index.md'), - 'web-ide-help-page-path' => help_page_path('user/project/web_ide/_index.md'), - 'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s, - 'default-branch' => project && project.default_branch, - 'project' => convert_to_project_entity_json(project), - 'preview-markdown-path' => project && preview_markdown_path(project), - 'web-terminal-svg-path' => image_path('illustrations/empty-state/empty-cloud-md.svg'), - 'web-terminal-help-path' => help_page_path('user/project/web_ide/_index.md'), - 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/_index.md'), - 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/_index.md') - } - end - - def convert_to_project_entity_json(project) - return unless project - - API::Entities::Project.represent(project, current_user: current_user).to_json - end - def has_dismissed_ide_environments_callout? current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance') end diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index a70168dc0d8..3005793722c 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -3,6 +3,7 @@ module AlertManagement class HttpIntegration < ApplicationRecord include ::Gitlab::Routing + include Gitlab::EncryptedAttribute LEGACY_IDENTIFIERS = %w[legacy legacy-prometheus].freeze @@ -10,7 +11,7 @@ module AlertManagement attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' attribute :endpoint_identifier, default: -> { SecureRandom.hex(8) } diff --git a/app/models/alerting/project_alerting_setting.rb b/app/models/alerting/project_alerting_setting.rb index 7e94d41137f..1da9faa65b1 100644 --- a/app/models/alerting/project_alerting_setting.rb +++ b/app/models/alerting/project_alerting_setting.rb @@ -4,13 +4,15 @@ require 'securerandom' module Alerting class ProjectAlertingSetting < ApplicationRecord + include Gitlab::EncryptedAttribute + belongs_to :project validates :token, presence: true attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' before_validation :ensure_token diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 98d49ef05cb..03ee4a3efba 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -6,6 +6,7 @@ class ApplicationSetting < ApplicationRecord include TokenAuthenticatable include ChronicDurationAttribute include Sanitizable + include Gitlab::EncryptedAttribute ignore_column :pre_receive_secret_detection_enabled, remove_with: '17.9', remove_after: '2025-02-15' @@ -854,14 +855,14 @@ class ApplicationSetting < ApplicationRecord attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, + key: :db_key_base_truncated, algorithm: 'aes-256-cbc', insecure_mode: true private_class_method def self.encryption_options_base_32_aes_256_gcm { mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: true } diff --git a/app/models/atlassian/identity.rb b/app/models/atlassian/identity.rb index 1ad7f657db1..3186f0c0c3a 100644 --- a/app/models/atlassian/identity.rb +++ b/app/models/atlassian/identity.rb @@ -2,6 +2,8 @@ module Atlassian class Identity < ApplicationRecord + include Gitlab::EncryptedAttribute + self.table_name = 'atlassian_identities' belongs_to :user @@ -11,14 +13,14 @@ module Atlassian attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false attr_encrypted :refresh_token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/app/models/bulk_imports/configuration.rb b/app/models/bulk_imports/configuration.rb index a4758ee75cf..00343cb4a3c 100644 --- a/app/models/bulk_imports/configuration.rb +++ b/app/models/bulk_imports/configuration.rb @@ -3,6 +3,8 @@ # Stores the authentication data required to access another GitLab instance on # behalf of a user, to import Groups and Projects directly from that instance. class BulkImports::Configuration < ApplicationRecord + include Gitlab::EncryptedAttribute + self.table_name = 'bulk_import_configurations' belongs_to :bulk_import, inverse_of: :configuration, optional: true @@ -12,11 +14,11 @@ class BulkImports::Configuration < ApplicationRecord allow_nil: true attr_encrypted :url, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, mode: :per_attribute_iv, algorithm: 'aes-256-gcm' attr_encrypted :access_token, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, mode: :per_attribute_iv, algorithm: 'aes-256-gcm' diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a187e81c882..f562189d0a9 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -165,7 +165,7 @@ module Ci preload( :job_artifacts_archive, :ci_stage, :job_artifacts, :runner, :tags, :runner_manager, :metadata, pipeline: :project, - user: [:user_preference, :user_detail, :followees] + user: [:user_preference, :user_detail, :followees, :followers] ) end diff --git a/app/models/cloud_connector/service_access_token.rb b/app/models/cloud_connector/service_access_token.rb index 9da6d614024..40d2b58f16a 100644 --- a/app/models/cloud_connector/service_access_token.rb +++ b/app/models/cloud_connector/service_access_token.rb @@ -2,6 +2,8 @@ module CloudConnector class ServiceAccessToken < ApplicationRecord + include Gitlab::EncryptedAttribute + self.table_name = 'service_access_tokens' scope :expired, -> { where('expires_at < :now', now: Time.current) } @@ -9,7 +11,7 @@ module CloudConnector attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb index 935d6238dba..671de45c66d 100644 --- a/app/models/clusters/integrations/prometheus.rb +++ b/app/models/clusters/integrations/prometheus.rb @@ -5,6 +5,7 @@ module Clusters class Prometheus < ApplicationRecord include ::Clusters::Concerns::PrometheusClient include AfterCommitQueue + include Gitlab::EncryptedAttribute self.table_name = 'clusters_integration_prometheus' self.primary_key = :cluster_id @@ -23,7 +24,7 @@ module Clusters attr_encrypted :alert_manager_token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' after_initialize :set_alert_manager_token, if: :new_record? diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb index e3da4e2a698..ac80e8494c0 100644 --- a/app/models/clusters/kubernetes_namespace.rb +++ b/app/models/clusters/kubernetes_namespace.rb @@ -3,6 +3,7 @@ module Clusters class KubernetesNamespace < ApplicationRecord include Gitlab::Kubernetes + include Gitlab::EncryptedAttribute self.table_name = 'clusters_kubernetes_namespaces' @@ -23,7 +24,7 @@ module Clusters attr_encrypted :service_account_token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, + key: :db_key_base_truncated, algorithm: 'aes-256-cbc' scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) } diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 85d4e89a5b2..8b8affd90b2 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -7,6 +7,7 @@ module Clusters include AfterCommitQueue include ReactiveCaching include NullifyIfBlank + include Gitlab::EncryptedAttribute RESERVED_NAMESPACES = %w[gitlab-managed-apps].freeze REQUIRED_K8S_MIN_VERSION = 23 @@ -30,12 +31,12 @@ module Clusters attr_encrypted :password, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, + key: :db_key_base_truncated, algorithm: 'aes-256-cbc' attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, + key: :db_key_base_truncated, algorithm: 'aes-256-cbc' before_validation :enforce_namespace_to_lower_case diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb index 969820459e3..04b2d8b19ab 100644 --- a/app/models/clusters/providers/aws.rb +++ b/app/models/clusters/providers/aws.rb @@ -5,6 +5,7 @@ module Clusters class Aws < ApplicationRecord include Gitlab::Utils::StrongMemoize include Clusters::Concerns::ProviderStatus + include Gitlab::EncryptedAttribute self.table_name = 'cluster_providers_aws' @@ -18,7 +19,7 @@ module Clusters attr_encrypted :secret_access_key, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' validates :role_arn, diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index 6f39037b947..9438f69aefe 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -4,6 +4,7 @@ module Clusters module Providers class Gcp < ApplicationRecord include Clusters::Concerns::ProviderStatus + include Gitlab::EncryptedAttribute self.table_name = 'cluster_providers_gcp' @@ -18,7 +19,7 @@ module Clusters attr_encrypted :access_token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, + key: :db_key_base_truncated, algorithm: 'aes-256-cbc' validates :gcp_project_id, diff --git a/app/models/concerns/integrations/base/integration.rb b/app/models/concerns/integrations/base/integration.rb index 53605d23312..bcfcfde3218 100644 --- a/app/models/concerns/integrations/base/integration.rb +++ b/app/models/concerns/integrations/base/integration.rb @@ -454,6 +454,8 @@ module Integrations include Integrations::ResetSecretFields include FromUnion include EachBatch + include Gitlab::EncryptedAttribute + extend SafeFormatHelper extend ::Gitlab::Utils::Override @@ -462,7 +464,7 @@ module Integrations attr_encrypted :properties, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', marshal: true, marshaler: ::Gitlab::Json, @@ -669,9 +671,10 @@ module Integrations def reencrypt_properties unless properties.nil? || properties.empty? - alg = self.class.attr_encrypted_attributes[:properties][:algorithm] - iv = generate_iv(alg) - ep = self.class.attr_encrypt(:properties, properties, { iv: iv }) + attr_encrypted_attributes = self.class.attr_encrypted_attributes[:properties] + key = dynamic_encryption_key_for_operation(attr_encrypted_attributes[:key]) + iv = generate_iv(attr_encrypted_attributes[:algorithm]) + ep = self.class.attr_encrypt(:properties, properties, { key: key, iv: iv }) end { 'encrypted_properties' => ep, 'encrypted_properties_iv' => iv } diff --git a/app/models/concerns/packages/debian/distribution_key.rb b/app/models/concerns/packages/debian/distribution_key.rb index 7023e2dcd37..3f22d4886ff 100644 --- a/app/models/concerns/packages/debian/distribution_key.rb +++ b/app/models/concerns/packages/debian/distribution_key.rb @@ -6,6 +6,8 @@ module Packages extend ActiveSupport::Concern included do + include Gitlab::EncryptedAttribute + belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :key validates :distribution, presence: true @@ -19,11 +21,11 @@ module Packages attr_encrypted :private_key, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' attr_encrypted :passphrase, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' private diff --git a/app/models/concerns/web_hooks/hook.rb b/app/models/concerns/web_hooks/hook.rb index 5660c697148..fae6efbd756 100644 --- a/app/models/concerns/web_hooks/hook.rb +++ b/app/models/concerns/web_hooks/hook.rb @@ -14,20 +14,21 @@ module WebHooks included do include Sortable include WebHooks::AutoDisabling + include Gitlab::EncryptedAttribute attr_encrypted :token, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32 + key: :db_key_base_32 attr_encrypted :url, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32 + key: :db_key_base_32 attr_encrypted :url_variables, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', marshal: true, marshaler: ::Gitlab::Json, @@ -36,7 +37,7 @@ module WebHooks attr_encrypted :custom_headers, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', marshal: true, marshaler: ::Gitlab::Json, @@ -168,11 +169,21 @@ module WebHooks end def decrypt_url_was - self.class.decrypt_url(encrypted_url_was, iv: Base64.decode64(encrypted_url_iv_was)) + options = { + key: dynamic_encryption_key_for_operation(attr_encrypted_attributes[:url][:key]), + iv: Base64.decode64(encrypted_url_iv_was) + } + + self.class.attr_decrypt(:url, encrypted_url_was, options) end def url_variables_were - self.class.decrypt_url_variables(encrypted_url_variables_was, iv: encrypted_url_variables_iv_was) + options = { + key: dynamic_encryption_key_for_operation(attr_encrypted_attributes[:url_variables][:key]), + iv: encrypted_url_variables_iv_was + } + + self.class.attr_decrypt(:url_variables, encrypted_url_variables_was, options) end def initialize_url_variables diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index abe883d6f1b..34b7641111f 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -5,6 +5,7 @@ module ErrorTracking include Gitlab::Utils::StrongMemoize include ReactiveCaching include Gitlab::Routing + include Gitlab::EncryptedAttribute SENTRY_API_ERROR_TYPE_BAD_REQUEST = 'bad_request_for_sentry_api' SENTRY_API_ERROR_TYPE_MISSING_KEYS = 'missing_keys_in_sentry_response' @@ -42,7 +43,7 @@ module ErrorTracking attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm' before_validation :reset_token diff --git a/app/models/grafana_integration.rb b/app/models/grafana_integration.rb index 37e69102521..b669d924ca9 100644 --- a/app/models/grafana_integration.rb +++ b/app/models/grafana_integration.rb @@ -1,12 +1,14 @@ # frozen_string_literal: true class GrafanaIntegration < ApplicationRecord + include Gitlab::EncryptedAttribute + belongs_to :project attr_encrypted :token, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32 + key: :db_key_base_32 before_validation :check_token_changes diff --git a/app/models/group.rb b/app/models/group.rb index b043a30e457..166f5ec5789 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -205,6 +205,19 @@ class Group < Namespace scope :with_users, -> { includes(:users) } + scope :active, -> do + non_archived.not_aimed_for_deletion + end + + scope :inactive, -> do + joins(:namespace_settings) + .left_joins(:deletion_schedule) + .where(<<~SQL) + #{reflections['namespace_settings'].table_name}.archived = TRUE + OR #{reflections['deletion_schedule'].table_name}.#{reflections['deletion_schedule'].foreign_key} IS NOT NULL + SQL + end + scope :with_non_archived_projects, -> { includes(:non_archived_projects) } scope :with_non_invite_group_members, -> { includes(:non_invite_group_members) } diff --git a/app/models/incident_management/project_incident_management_setting.rb b/app/models/incident_management/project_incident_management_setting.rb index b6da93508c2..f7a91e83e8d 100644 --- a/app/models/incident_management/project_incident_management_setting.rb +++ b/app/models/incident_management/project_incident_management_setting.rb @@ -3,6 +3,7 @@ module IncidentManagement class ProjectIncidentManagementSetting < ApplicationRecord include Gitlab::Utils::StrongMemoize + include Gitlab::EncryptedAttribute belongs_to :project @@ -12,7 +13,7 @@ module IncidentManagement attr_encrypted :pagerduty_token, mode: :per_attribute_iv, - key: ::Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, # No need to encode for binary column https://github.com/attr-encrypted/attr_encrypted#the-encode-encode_iv-encode_salt-and-default_encoding-options encode_iv: false diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb index 0da8482fea4..8141839407e 100644 --- a/app/models/jira_connect_installation.rb +++ b/app/models/jira_connect_installation.rb @@ -2,11 +2,12 @@ class JiraConnectInstallation < ApplicationRecord include Gitlab::Routing + include Gitlab::EncryptedAttribute attr_encrypted :shared_secret, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32 + key: :db_key_base_32 has_many :subscriptions, class_name: 'JiraConnectSubscription' diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 841229b5421..f4d4e5d3b7c 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -4,6 +4,7 @@ class PagesDomain < ApplicationRecord include Presentable include FromUnion include AfterCommitQueue + include Gitlab::EncryptedAttribute VERIFICATION_KEY = 'gitlab-pages-verification-code' VERIFICATION_THRESHOLD = 3.days.freeze @@ -48,7 +49,7 @@ class PagesDomain < ApplicationRecord attr_encrypted :key, mode: :per_attribute_iv_and_salt, insecure_mode: true, - key: Settings.attr_encrypted_db_key_base, + key: :db_key_base, algorithm: 'aes-256-cbc' scope :for_project, ->(project) { where(project: project) } diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb index 80688e8d247..4872bc55e0c 100644 --- a/app/models/pages_domain_acme_order.rb +++ b/app/models/pages_domain_acme_order.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PagesDomainAcmeOrder < ApplicationRecord + include Gitlab::EncryptedAttribute + belongs_to :pages_domain scope :expired, -> { where("expires_at < ?", Time.current) } @@ -14,7 +16,7 @@ class PagesDomainAcmeOrder < ApplicationRecord attr_encrypted :private_key, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: true diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 30cdb6ea62d..d0ab049678c 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -3,6 +3,8 @@ require 'carrierwave/orm/activerecord' class ProjectImportData < ApplicationRecord + include Gitlab::EncryptedAttribute + prepend_mod_with('ProjectImportData') # rubocop: disable Cop/InjectEnterpriseEditionModule # Timeout strategy can only be changed via API, currently only with GitHub and BitBucket Server @@ -12,7 +14,7 @@ class ProjectImportData < ApplicationRecord belongs_to :project, inverse_of: :import_data attr_encrypted :credentials, - key: Settings.attr_encrypted_db_key_base, + key: :db_key_base, marshal: true, encode: true, mode: :per_attribute_iv_and_salt, diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb index 25c38936e93..e11d832b13a 100644 --- a/app/models/project_setting.rb +++ b/app/models/project_setting.rb @@ -5,6 +5,7 @@ class ProjectSetting < ApplicationRecord include EachBatch include CascadingProjectSettingAttribute include Projects::SquashOption + include Gitlab::EncryptedAttribute ALLOWED_TARGET_PLATFORMS = %w[ios osx tvos watchos android].freeze @@ -18,14 +19,14 @@ class ProjectSetting < ApplicationRecord attr_encrypted :cube_api_key, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false attr_encrypted :product_analytics_configurator_connection_string, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 5914799a3e3..819480a6e7d 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -4,6 +4,7 @@ class RemoteMirror < ApplicationRecord include AfterCommitQueue include MirrorAuthentication include SafeUrl + include Gitlab::EncryptedAttribute MAX_FIRST_RUNTIME = 3.hours MAX_INCREMENTAL_RUNTIME = 1.hour @@ -11,7 +12,7 @@ class RemoteMirror < ApplicationRecord UNPROTECTED_BACKOFF_DELAY = 5.minutes attr_encrypted :credentials, - key: Settings.attr_encrypted_db_key_base, + key: :db_key_base, marshal: true, encode: true, mode: :per_attribute_iv_and_salt, diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb index f3c201f58d1..2cf037b873e 100644 --- a/app/models/service_desk/custom_email_credential.rb +++ b/app/models/service_desk/custom_email_credential.rb @@ -2,6 +2,8 @@ module ServiceDesk class CustomEmailCredential < ApplicationRecord + include Gitlab::EncryptedAttribute + # Used to explicitly set the SMTP AUTH method. # If nil Net::SMTP will choose one of methods listed by the SMTP server. enum smtp_authentication: { @@ -13,13 +15,13 @@ module ServiceDesk attr_encrypted :smtp_username, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, encode: false, encode_iv: false attr_encrypted :smtp_password, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, encode: false, encode_iv: false diff --git a/app/models/service_desk/custom_email_verification.rb b/app/models/service_desk/custom_email_verification.rb index a03c984c3a6..62ab63d7339 100644 --- a/app/models/service_desk/custom_email_verification.rb +++ b/app/models/service_desk/custom_email_verification.rb @@ -2,6 +2,8 @@ module ServiceDesk class CustomEmailVerification < ApplicationRecord + include Gitlab::EncryptedAttribute + TIMEFRAME = 30.minutes STATES = { started: 0, finished: 1, failed: 2 }.freeze @@ -18,7 +20,7 @@ module ServiceDesk attr_encrypted :token, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, encode: false, encode_iv: false diff --git a/app/models/slack_integration.rb b/app/models/slack_integration.rb index 8e852aa4fa3..7900203b993 100644 --- a/app/models/slack_integration.rb +++ b/app/models/slack_integration.rb @@ -2,6 +2,7 @@ class SlackIntegration < ApplicationRecord include EachBatch + include Gitlab::EncryptedAttribute ALL_FEATURES = %i[commands notifications].freeze @@ -21,7 +22,7 @@ class SlackIntegration < ApplicationRecord attr_encrypted :bot_access_token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 93acff57547..5275c44a0d4 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -20,6 +20,7 @@ class Snippet < ApplicationRecord include CreatedAtFilterable include EachBatch include Import::HasImportSource + include Gitlab::EncryptedAttribute MAX_FILE_COUNT = 10 @@ -94,7 +95,7 @@ class Snippet < ApplicationRecord attr_spammable :description, spam_description: true attr_encrypted :secret_token, - key: Settings.attr_encrypted_db_key_base_truncated, + key: :db_key_base_truncated, mode: :per_attribute_iv, algorithm: 'aes-256-cbc' diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml index b9a95d18804..bed087db1b5 100644 --- a/app/views/ide/index.html.haml +++ b/app/views/ide/index.html.haml @@ -1,23 +1,6 @@ - disable_fixed_body_scroll - page_title _("IDE"), @project.full_name - add_page_specific_style 'page_bundles/web_ide_loader' - -// The block below is for the Web IDE -// See: https://gitlab.com/groups/gitlab-org/-/epics/7683 -- unless use_new_web_ide? - - @breadcrumb_title = _("IDE") - - @breadcrumb_link = '#' - - @no_container = true - - @content_wrapper_class = 'pb-0' - - add_to_breadcrumbs(s_('Navigation|Your work'), root_path) - - nav 'your_work' # Couldn't get the `project` nav to work easily - - add_page_specific_style 'page_bundles/build' - - add_page_specific_style 'page_bundles/ide' - - add_page_specific_style 'page_bundles/terminal' - - - content_for :prefetch_asset_tags do - - webpack_preload_asset_tag('monaco') - - data = ide_data(project: @project, fork_info: @fork_info, params: params) = render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the Web IDE') } diff --git a/app/workers/namespaces/enable_descendants_cache_cron_worker.rb b/app/workers/namespaces/enable_descendants_cache_cron_worker.rb index 0b8801e84f9..fb9005f5e28 100644 --- a/app/workers/namespaces/enable_descendants_cache_cron_worker.rb +++ b/app/workers/namespaces/enable_descendants_cache_cron_worker.rb @@ -68,7 +68,7 @@ module Namespaces def persist(ids_to_cache) ids_to_cache.each_slice(PERSIST_SLICE_SIZE) do |slice| - Namespaces::Descendants.upsert_all(slice.map { |id| { namespace_id: id } }) + Namespaces::Descendants.upsert_all(slice.map { |id| { namespace_id: id, outdated_at: Time.current } }) end end diff --git a/config/feature_flags/development/vscode_web_ide.yml b/config/feature_flags/development/vscode_web_ide.yml deleted file mode 100644 index 93f4141ed97..00000000000 --- a/config/feature_flags/development/vscode_web_ide.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: vscode_web_ide -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95169 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371084 -milestone: '15.4' -type: development -group: group::ide -default_enabled: true diff --git a/db/migrate/20241017160504_generate_ci_job_token_signing_key.rb b/db/migrate/20241017160504_generate_ci_job_token_signing_key.rb index 5eb372af695..1ab1446dc02 100644 --- a/db/migrate/20241017160504_generate_ci_job_token_signing_key.rb +++ b/db/migrate/20241017160504_generate_ci_job_token_signing_key.rb @@ -5,9 +5,11 @@ class GenerateCiJobTokenSigningKey < Gitlab::Database::Migration[2.2] restrict_gitlab_migration gitlab_schema: :gitlab_main class ApplicationSetting < MigrationRecord + include Gitlab::EncryptedAttribute + attr_encrypted :ci_job_token_signing_key, { mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/db/migrate/20241017160505_regenerate_ci_job_token_signing_key.rb b/db/migrate/20241017160505_regenerate_ci_job_token_signing_key.rb index a0151b6be4b..54f0db9746b 100644 --- a/db/migrate/20241017160505_regenerate_ci_job_token_signing_key.rb +++ b/db/migrate/20241017160505_regenerate_ci_job_token_signing_key.rb @@ -8,9 +8,11 @@ class RegenerateCiJobTokenSigningKey < Gitlab::Database::Migration[2.2] restrict_gitlab_migration gitlab_schema: :gitlab_main class ApplicationSetting < MigrationRecord + include Gitlab::EncryptedAttribute + attr_encrypted :ci_job_token_signing_key, { mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/db/migrate/20250228183319_migrate_vscode_extension_marketplace_feature_flag_to_data.rb b/db/migrate/20250228183319_migrate_vscode_extension_marketplace_feature_flag_to_data.rb index eb0a681f273..68b6631177e 100644 --- a/db/migrate/20250228183319_migrate_vscode_extension_marketplace_feature_flag_to_data.rb +++ b/db/migrate/20250228183319_migrate_vscode_extension_marketplace_feature_flag_to_data.rb @@ -47,7 +47,6 @@ class MigrateVSCodeExtensionMarketplaceFeatureFlagToData < Gitlab::Database::Mig Feature.enabled?(:web_ide_extensions_marketplace, nil) && # NOTE: We only want to migrate instances that have **explicitly** opted in to the early # extensions marketplace experience (not just enabled by default feature flag). - Feature.persisted_name?(:web_ide_extensions_marketplace) && - Feature.enabled?(:vscode_web_ide, nil) + Feature.persisted_name?(:web_ide_extensions_marketplace) end end diff --git a/db/post_migrate/20250422093734_outdate_namespace_descendants_cache.rb b/db/post_migrate/20250422093734_outdate_namespace_descendants_cache.rb new file mode 100644 index 00000000000..9b346a4743d --- /dev/null +++ b/db/post_migrate/20250422093734_outdate_namespace_descendants_cache.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class OutdateNamespaceDescendantsCache < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + milestone '18.0' + + def up + table = Gitlab::Database::PostgresPartitionedTable.find_by_name_in_current_schema("namespace_descendants") + table.postgres_partitions.each do |partition| + partition_name = "#{quote_table_name(partition.schema)}.#{quote_table_name(partition.name)}" + with_lock_retries do + execute "UPDATE #{partition_name} SET outdated_at = NOW()" + end + end + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20250422093734 b/db/schema_migrations/20250422093734 new file mode 100644 index 00000000000..aa0a46ac57d --- /dev/null +++ b/db/schema_migrations/20250422093734 @@ -0,0 +1 @@ +d5ffdd5d041245f597dd74a033354c5302a808d8d03014a2f65e57be0a2fba16 \ No newline at end of file diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index f7c05a6072f..adbadb04262 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -558,7 +558,7 @@ Returns [`CurrentUser`](#currentuser). ### `Query.customField` -Find a custom field by its ID. Available only when feature flag `custom_fields_feature` is enabled. +Find a custom field by its ID. {{< details >}} **Introduced** in GitLab 17.10. @@ -27751,7 +27751,7 @@ four standard [pagination arguments](#pagination-arguments): ##### `Group.customField` -A custom field configured for the group. Available only when feature flag `custom_fields_feature` is enabled. +A custom field configured for the group. {{< details >}} **Introduced** in GitLab 17.6. @@ -27768,7 +27768,7 @@ Returns [`CustomField`](#customfield). ##### `Group.customFields` -Custom fields configured for the group. Available only when feature flag `custom_fields_feature` is enabled. +Custom fields configured for the group. {{< details >}} **Introduced** in GitLab 17.5. @@ -32870,7 +32870,7 @@ four standard [pagination arguments](#pagination-arguments): ##### `Namespace.customFields` -Custom fields configured for the namespace. Available only when feature flag `custom_fields_feature` is enabled. +Custom fields configured for the namespace. {{< details >}} **Introduced** in GitLab 17.10. diff --git a/doc/api/groups.md b/doc/api/groups.md index 54bd5c62ac2..4b264c0ba0f 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -311,6 +311,7 @@ Parameters: | `top_level_only` | boolean | no | Limit to top-level groups, excluding all subgroups | | `repository_storage` | string | no | Filter by repository storage used by the group _(administrators only)_. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/419643) in GitLab 16.3. Premium and Ultimate only. | | `marked_for_deletion_on` | date | no | Filter by date when group was marked for deletion. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429315) in GitLab 17.1. Premium and Ultimate only. | +| `active` | boolean | no | Limit by groups that are not archived and not marked for deletion. | ```plaintext GET /groups @@ -929,6 +930,7 @@ Parameters: | `owned` | boolean | no | Limit to groups explicitly owned by the current user | | `min_access_level` | integer | no | Limit to groups where current user has at least this [role (`access_level`)](members.md#roles) | | `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. | +| `active` | boolean | no | Limit by groups that are not archived and not marked for deletion. | ```plaintext GET /groups/:id/subgroups @@ -1006,6 +1008,7 @@ Parameters: | `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) | | `owned` | boolean | no | Limit to groups explicitly owned by the current user | | `min_access_level` | integer | no | Limit to groups where current user has at least this [role (`access_level`)](members.md#roles) | +| `active` | boolean | no | Limit by groups that are not archived and not marked for deletion. | ```plaintext GET /groups/:id/descendant_groups diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index 1d37055ddba..a0d00906a3b 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -1061,6 +1061,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by groups that are not archived and not marked for deletion + type: boolean + required: false - in: query name: repository_storage description: Filter by repository storage used by the group @@ -1800,6 +1805,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by groups that are not archived and not marked for deletion + type: boolean + required: false - in: query name: repository_storage description: Filter by repository storage used by the group @@ -1937,6 +1947,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by groups that are not archived and not marked for deletion + type: boolean + required: false - in: query name: repository_storage description: Filter by repository storage used by the group @@ -31969,6 +31984,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by projects that are not archived and not marked for deletion + type: boolean + required: false - in: query name: wiki_checksum_failed description: Limit by projects where wiki checksum is failed @@ -32452,6 +32472,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by projects that are not archived and not marked for deletion + type: boolean + required: false - in: query name: wiki_checksum_failed description: Limit by projects where wiki checksum is failed @@ -39917,6 +39942,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by projects that are not archived and not marked for deletion + type: boolean + required: false - in: query name: wiki_checksum_failed description: Limit by projects where wiki checksum is failed @@ -40248,6 +40278,11 @@ paths: type: string format: date required: false + - in: query + name: active + description: Limit by projects that are not archived and not marked for deletion + type: boolean + required: false - in: query name: wiki_checksum_failed description: Limit by projects where wiki checksum is failed diff --git a/doc/api/projects.md b/doc/api/projects.md index c0fff27edde..bcc17931182 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -369,39 +369,40 @@ GET /projects Supported attributes: -| Attribute | Type | Required | Description | -|:------------------------------|:---------|:---------|:------------| -| `archived` | boolean | No | Limit by archived status. | -| `id_after` | integer | No | Limit results to projects with IDs greater than the specified ID. | -| `id_before` | integer | No | Limit results to projects with IDs less than the specified ID. | -| `imported` | boolean | No | Limit results to projects which were imported from external systems by current user. | -| `include_hidden` | boolean | No | Include hidden projects. _(administrators only)_ Premium and Ultimate only. | -| `include_pending_delete` | boolean | No | Include projects pending deletion. _(administrators only)_ | -| `last_activity_after` | datetime | No | Limit results to projects with last activity after specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) | -| `last_activity_before` | datetime | No | Limit results to projects with last activity before specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) | -| `membership` | boolean | No | Limit by projects that the current user is a member of. | -| `min_access_level` | integer | No | Limit by current user minimal [role (`access_level`)](members.md#roles). | +| Attribute | Type | Required | Description | +|:------------------------------|:---------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `archived` | boolean | No | Limit by archived status. | +| `id_after` | integer | No | Limit results to projects with IDs greater than the specified ID. | +| `id_before` | integer | No | Limit results to projects with IDs less than the specified ID. | +| `imported` | boolean | No | Limit results to projects which were imported from external systems by current user. | +| `include_hidden` | boolean | No | Include hidden projects. _(administrators only)_ Premium and Ultimate only. | +| `include_pending_delete` | boolean | No | Include projects pending deletion. _(administrators only)_ | +| `last_activity_after` | datetime | No | Limit results to projects with last activity after specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) | +| `last_activity_before` | datetime | No | Limit results to projects with last activity before specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`) | +| `membership` | boolean | No | Limit by projects that the current user is a member of. | +| `min_access_level` | integer | No | Limit by current user minimal [role (`access_level`)](members.md#roles). | | `order_by` | string | No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `star_count`, `last_activity_at`, or `similarity` fields. `repository_size`, `storage_size`, `packages_size` or `wiki_size` fields are only allowed for administrators. `similarity` is only available when searching and is limited to projects that the current user is a member of. Default is `created_at`. | -| `owned` | boolean | No | Limit by projects explicitly owned by the current user. | -| `repository_checksum_failed` | boolean | No | Limit projects where the repository checksum calculation has failed. Premium and Ultimate only. | -| `repository_storage` | string | No | Limit results to projects stored on `repository_storage`. _(administrators only)_ | -| `search_namespaces` | boolean | No | Include ancestor namespaces when matching search criteria. Default is `false`. | -| `search` | string | No | Return list of projects with a `path`, `name`, or `description` matching the search criteria (case-insensitive, substring match). Multiple terms can be provided, separated by an escaped space, either `+` or `%20`, and will be ANDed together. Example: `one+two` will match substrings `one` and `two` (in any order). | -| `simple` | boolean | No | Return only limited fields for each project. This operation is a no-op without authentication where only simple fields are returned. | -| `sort` | string | No | Return projects sorted in `asc` or `desc` order. Default is `desc`. | -| `starred` | boolean | No | Limit by projects starred by the current user. | -| `statistics` | boolean | No | Include project statistics. Available only to users with at least the Reporter role. | -| `topic_id` | integer | No | Limit results to projects with the assigned topic given by the topic ID. | -| `topic` | string | No | Comma-separated topic names. Limit results to projects that match all of given topics. See `topics` attribute. | -| `updated_after` | datetime | No | Limit results to projects last updated after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. For this filter to work, you must also provide `updated_at` as the `order_by` attribute. | -| `updated_before` | datetime | No | Limit results to projects last updated before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. For this filter to work, you must also provide `updated_at` as the `order_by` attribute. | -| `visibility` | string | No | Limit by visibility `public`, `internal`, or `private`. | -| `wiki_checksum_failed` | boolean | No | Limit projects where the wiki checksum calculation has failed. Premium and Ultimate only. | -| `with_custom_attributes` | boolean | No | Include [custom attributes](custom_attributes.md) in response. _(administrator only)_ | -| `with_issues_enabled` | boolean | No | Limit by enabled issues feature. | -| `with_merge_requests_enabled` | boolean | No | Limit by enabled merge requests feature. | -| `with_programming_language` | string | No | Limit by projects which use the given programming language. | -| `marked_for_deletion_on` | date | No | Filter by date when project was marked for deletion. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/463939) in GitLab 17.1. Premium and Ultimate only. | +| `owned` | boolean | No | Limit by projects explicitly owned by the current user. | +| `repository_checksum_failed` | boolean | No | Limit projects where the repository checksum calculation has failed. Premium and Ultimate only. | +| `repository_storage` | string | No | Limit results to projects stored on `repository_storage`. _(administrators only)_ | +| `search_namespaces` | boolean | No | Include ancestor namespaces when matching search criteria. Default is `false`. | +| `search` | string | No | Return list of projects with a `path`, `name`, or `description` matching the search criteria (case-insensitive, substring match). Multiple terms can be provided, separated by an escaped space, either `+` or `%20`, and will be ANDed together. Example: `one+two` will match substrings `one` and `two` (in any order). | +| `simple` | boolean | No | Return only limited fields for each project. This operation is a no-op without authentication where only simple fields are returned. | +| `sort` | string | No | Return projects sorted in `asc` or `desc` order. Default is `desc`. | +| `starred` | boolean | No | Limit by projects starred by the current user. | +| `statistics` | boolean | No | Include project statistics. Available only to users with at least the Reporter role. | +| `topic_id` | integer | No | Limit results to projects with the assigned topic given by the topic ID. | +| `topic` | string | No | Comma-separated topic names. Limit results to projects that match all of given topics. See `topics` attribute. | +| `updated_after` | datetime | No | Limit results to projects last updated after the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. For this filter to work, you must also provide `updated_at` as the `order_by` attribute. | +| `updated_before` | datetime | No | Limit results to projects last updated before the specified time. Format: ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393979) in GitLab 15.10. For this filter to work, you must also provide `updated_at` as the `order_by` attribute. | +| `visibility` | string | No | Limit by visibility `public`, `internal`, or `private`. | +| `wiki_checksum_failed` | boolean | No | Limit projects where the wiki checksum calculation has failed. Premium and Ultimate only. | +| `with_custom_attributes` | boolean | No | Include [custom attributes](custom_attributes.md) in response. _(administrator only)_ | +| `with_issues_enabled` | boolean | No | Limit by enabled issues feature. | +| `with_merge_requests_enabled` | boolean | No | Limit by enabled merge requests feature. | +| `with_programming_language` | string | No | Limit by projects which use the given programming language. | +| `marked_for_deletion_on` | date | No | Filter by date when project was marked for deletion. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/463939) in GitLab 17.1. Premium and Ultimate only. | +| `active` | boolean | No | Limit by projects that are not archived and not marked for deletion. | This endpoint supports [keyset pagination](rest/_index.md#keyset-based-pagination) for selected `order_by` options. diff --git a/doc/integration/exact_code_search/zoekt.md b/doc/integration/exact_code_search/zoekt.md index d0d045116c4..8442ce96819 100644 --- a/doc/integration/exact_code_search/zoekt.md +++ b/doc/integration/exact_code_search/zoekt.md @@ -72,6 +72,20 @@ To enable [exact code search](../../user/search/exact_code_search.md) in GitLab: ## Check indexing status +{{< history >}} + +- Stopping indexing when Zoekt node storage exceeds the critical watermark [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/504945) in GitLab 17.7 [with a flag](../../administration/feature_flags.md) named `zoekt_critical_watermark_stop_indexing`. Disabled by default. +- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/505334) in GitLab 18.0. + +{{< /history >}} + +{{< alert type="flag" >}} + +The availability of this feature is controlled by a feature flag. +For more information, see the history. + +{{< /alert >}} + Prerequisites: - You must have administrator access to the instance. diff --git a/doc/user/work_items/custom_fields.md b/doc/user/work_items/custom_fields.md index 6881f815fab..402074e6c33 100644 --- a/doc/user/work_items/custom_fields.md +++ b/doc/user/work_items/custom_fields.md @@ -10,7 +10,6 @@ title: Custom fields - Tier: Premium, Ultimate - Offering: GitLab.com, GitLab Self-Managed -- Status: Beta {{< /details >}} @@ -18,16 +17,10 @@ title: Custom fields - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/479571) in GitLab 17.11 [with a flag](../../administration/feature_flags.md) named `custom_fields_feature`. Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated. +- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/479571) in GitLab 18.0. Feature flag `custom_fields_feature` removed. {{< /history >}} -{{< alert type="flag" >}} - -The availability of this feature is controlled by a feature flag. -For more information, see the history. - -{{< /alert >}} - Custom fields add specialized information to work items, such as issues and epics, that match your specific planning needs. Configure custom fields for a group to track data points like business value, risk assessment, priority ranking, or team attributes. These fields appear in all work items across the group, its subgroups, and projects. diff --git a/lib/api/groups.rb b/lib/api/groups.rb index f6295634344..3c346c22a73 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -33,6 +33,7 @@ module API optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Minimum access level of authenticated user' optional :top_level_only, type: Boolean, desc: 'Only include top-level groups' optional :marked_for_deletion_on, type: Date, desc: 'Return groups that are marked for deletion on this date' + optional :active, type: Boolean, desc: 'Limit by groups that are not archived and not marked for deletion' use :optional_group_list_params_ee use :pagination end @@ -61,7 +62,8 @@ module API [:all_available, :custom_attributes, :owned, :min_access_level, - :include_parent_descendants, :search, :visibility, :archived, :marked_for_deletion_on] + :include_parent_descendants, :search, :visibility, :archived, + :active, :marked_for_deletion_on] end # This is a separate method so that EE can extend its behaviour, without diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3c403b471b8..ce38a30e7ab 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -810,6 +810,7 @@ module API finder_params[:non_public] = true if params[:membership].present? finder_params[:starred] = true if params[:starred].present? finder_params[:archived] = archived_param unless params[:archived].nil? + finder_params[:active] = params[:active] unless params[:active].nil? finder_params end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 715027f9e53..fb5a3f214e9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -181,6 +181,7 @@ module API optional :updated_after, type: DateTime, desc: 'Return projects updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' optional :include_pending_delete, type: Boolean, desc: 'Include projects in pending delete state. Can only be set by admins' optional :marked_for_deletion_on, type: Date, desc: 'Date when the project was marked for deletion' + optional :active, type: Boolean, desc: 'Limit by projects that are not archived and not marked for deletion' use :optional_filter_params_ee end diff --git a/lib/gitlab/database/migration.rb b/lib/gitlab/database/migration.rb index 850476f8aad..36ad50efc0d 100644 --- a/lib/gitlab/database/migration.rb +++ b/lib/gitlab/database/migration.rb @@ -79,6 +79,10 @@ module Gitlab include Gitlab::Database::Migrations::MilestoneMixin end + class V2_3 < V2_2 + include Gitlab::Database::MigrationHelpers::RequireDisableDdlTransactionForMultipleLocks + end + def self.[](version) version = version.to_s name = "V#{version.tr('.', '_')}" @@ -89,7 +93,7 @@ module Gitlab # The current version to be used in new migrations def self.current_version - 2.2 + 2.3 end end end diff --git a/lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks.rb b/lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks.rb new file mode 100644 index 00000000000..63226c1ce22 --- /dev/null +++ b/lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module MigrationHelpers + module RequireDisableDdlTransactionForMultipleLocks + extend ActiveSupport::Concern + + LOCK_ACQUIRING_COMMANDS = %w[ALTER CREATE DROP TRUNCATE LOCK UPDATE DELETE INSERT].freeze + + # Reference: https://www.postgresql.org/docs/current/explicit-locking.html + LOCK_TYPES = { + high_severity: [ + 'AccessExclusiveLock', # Conflicts with all lock modes + 'ExclusiveLock' # Conflicts with all except ROW SHARE + ], + + low_severity: [ + 'RowShareLock', # Conflicts with EXCLUSIVE + 'AccessShareLock' # Conflicts with ACCESS EXCLUSIVE only + ] + }.freeze + + class_methods do + def skip_require_disable_ddl_transactions! + @skip_require_disable_ddl_transactions = true + end + + def skip_require_disable_ddl_transactions? + @skip_require_disable_ddl_transactions + end + end + + def exec_migration(connection, direction) + return super if should_skip_check? + + # In-memory tracking structures + statement_tracking = [] + tables_locked_up_till_now = Set.new + + begin + # Subscribe to SQL execution to track each statement + subscription_id = ActiveSupport::Notifications.subscribe('sql.active_record') do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + sql = event.payload[:sql].strip + + next if should_skip_sql_statement?(sql) + + newly_locked_tables = [] + if likely_to_acquire_locks?(sql) + newly_locked_tables = check_current_locks(connection).excluding(tables_locked_up_till_now.to_a) + end + + newly_locked_tables.each { |table| tables_locked_up_till_now.add(table) } + + # Record new statement + current_statement = { + number: 1 + statement_tracking.size, + sql: sql, + locked_tables: newly_locked_tables.uniq + } + statement_tracking << current_statement + end + + # Run the migration + super.tap do + # After the migration completes, analyze the collected lock data + verify_single_table_per_statement(statement_tracking) + end + ensure + # Cleanup + ActiveSupport::Notifications.unsubscribe(subscription_id) if subscription_id + end + end + + private + + def should_skip_check? + return true if disable_ddl_transaction + + self.class.skip_require_disable_ddl_transactions? + end + + def should_skip_sql_statement?(sql) + sql.empty? || + sql.start_with?('SET', 'BEGIN', 'COMMIT', 'ROLLBACK') || + sql.include?('pg_locks') || + (sql.start_with?('SELECT') && sql.exclude?('FOR UPDATE') && sql.exclude?('FOR SHARE')) + end + + def likely_to_acquire_locks?(sql) + first_word = sql.split(' ').first&.upcase + + LOCK_ACQUIRING_COMMANDS.include?(first_word) || + sql.include?('FOR UPDATE') || + sql.include?('FOR SHARE') || + sql.match?(/CREATE\s+(OR\s+REPLACE\s+)?TRIGGER/i) + end + + def check_current_locks(connection) + # Get current locks excluding system tables and read-only locks + low_severity_locks = LOCK_TYPES[:low_severity].map { |lock| connection.quote(lock) }.join(', ') + + locks = connection.execute(<<-SQL) + SELECT DISTINCT relation::regclass AS table_name + FROM pg_locks + WHERE relation IS NOT NULL + AND pid = pg_backend_pid() + AND relation::regclass::text NOT LIKE 'pg_%' + AND relation::regclass::text NOT LIKE 'information_schema.%' + AND relation::regclass::text NOT IN ('schema_migrations', 'ar_internal_metadata') + AND mode NOT IN (#{low_severity_locks}) + SQL + + locks.pluck('table_name').map(&:to_s).uniq + end + + def verify_single_table_per_statement(statement_tracking) + # Get all tables locked across all statements + table_lock_statements = [] + + statement_tracking.each do |statement| + # Skip statements with no locks + next if statement[:locked_tables].empty? + + # Check if this specific statement locked tables + table_lock_statements << statement if statement[:locked_tables].any? + end + + # Check if we have locks on multiple tables across the entire migration + return unless table_lock_statements.many? + + error_message = ["This migration locks multiple tables across different statements:"] + error_message << table_lock_statements.flatten.uniq.to_a.join(', ') + + error_message << "\nTables locked by each statement:" + statement_tracking.each do |stmt| + next if stmt[:locked_tables].empty? + + error_message << " Statement ##{stmt[:number]}: #{stmt[:locked_tables].join(', ')}" + error_message << " SQL: #{stmt[:sql]}" + end + + error_message << "\nPlease do one of the following:" + error_message << " - Split this migration into smaller migrations that each lock only a single table" + error_message << " - Disable the outer transaction by calling disable_ddl_transaction!" + error_message << + " - Disable check if you feel it is a false positive by calling skip_require_disable_ddl_transactions!" + raise error_message.join("\n") + end + end + end + end +end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 0dacd8ad920..b418c075a39 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -81,7 +81,6 @@ module Gitlab # Initialize gon.features with any flags that should be # made globally available to the frontend - push_frontend_feature_flag(:vscode_web_ide, current_user) push_frontend_feature_flag(:ui_for_organizations, current_user) push_frontend_feature_flag(:organization_switching, current_user) push_frontend_feature_flag(:find_and_replace, current_user) diff --git a/lib/web_ide/default_oauth_application.rb b/lib/web_ide/default_oauth_application.rb index 959db953a8b..389ad526412 100644 --- a/lib/web_ide/default_oauth_application.rb +++ b/lib/web_ide/default_oauth_application.rb @@ -3,10 +3,6 @@ module WebIde module DefaultOauthApplication class << self - def feature_enabled?(current_user) - Feature.enabled?(:vscode_web_ide, current_user) - end - def oauth_application application_settings.web_ide_oauth_application end diff --git a/lib/web_ide/extension_marketplace.rb b/lib/web_ide/extension_marketplace.rb index a2da3bb3731..5791bede7b2 100644 --- a/lib/web_ide/extension_marketplace.rb +++ b/lib/web_ide/extension_marketplace.rb @@ -8,8 +8,7 @@ module WebIde def self.feature_enabled_for_any_user? # note: Intentionally pass `nil` here since we don't have a user in scope feature_enabled_from_application_settings?(user: nil) && - feature_flag_enabled_for_any_actor?(:web_ide_extensions_marketplace) && - feature_flag_enabled_for_any_actor?(:vscode_web_ide) + feature_flag_enabled_for_any_actor?(:web_ide_extensions_marketplace) end # Returns true if the extensions marketplace feature is enabled for the given user @@ -93,8 +92,7 @@ module WebIde # @param user [User] # @return [Boolean] def self.feature_enabled_from_flags?(user:) - Feature.enabled?(:web_ide_extensions_marketplace, user) && - Feature.enabled?(:vscode_web_ide, user) + Feature.enabled?(:web_ide_extensions_marketplace, user) end private_class_method :feature_flag_enabled_for_any_actor?, :should_use_application_settings?, diff --git a/rubocop/cop/migration/versioned_migration_class.rb b/rubocop/cop/migration/versioned_migration_class.rb index 3fc1d595398..00c4266f19b 100644 --- a/rubocop/cop/migration/versioned_migration_class.rb +++ b/rubocop/cop/migration/versioned_migration_class.rb @@ -8,8 +8,8 @@ module RuboCop class VersionedMigrationClass < RuboCop::Cop::Base include MigrationHelpers - ENFORCED_SINCE = 2023_11_01_02_15_00 - CURRENT_MIGRATION_VERSION = 2.2 # Should be the same value as Gitlab::Database::Migration.current_version + ENFORCED_SINCE = 2025_04_27_00_00_00 + CURRENT_MIGRATION_VERSION = 2.3 # Should be the same value as Gitlab::Database::Migration.current_version DOC_LINK = "https://docs.gitlab.com/ee/development/migration_style_guide.html#migration-helpers-and-versioning" MSG_INHERIT = "Don't inherit from ActiveRecord::Migration or old versions of Gitlab::Database::Migration. " \ diff --git a/spec/controllers/concerns/rapid_diffs/streaming_resource_spec.rb b/spec/controllers/concerns/rapid_diffs/streaming_resource_spec.rb index 697ecb1fdfb..1dcabf7a7c5 100644 --- a/spec/controllers/concerns/rapid_diffs/streaming_resource_spec.rb +++ b/spec/controllers/concerns/rapid_diffs/streaming_resource_spec.rb @@ -49,33 +49,46 @@ RSpec.describe RapidDiffs::StreamingResource, type: :controller, feature_categor end end - describe '#stream_diff_files' do + describe '#diffs' do let(:controller_instance) { controller.new } let(:mock_resource) { instance_double(::Commit) } let(:mock_diffs) { instance_double(Gitlab::Diff::FileCollection::Commit, diff_files: diff_files) } - let(:diff_files) { [] } - let(:response) do - instance_double(ActionDispatch::Response, - stream: instance_double(ActionDispatch::Response::Buffer, write: nil)) - end + let(:diff_files) { [{}] } + let(:stream) { instance_double(ActionDispatch::Response::Buffer, write: nil, close: nil) } + let(:response) { instance_double(ActionDispatch::Response, stream: stream) } let(:empty_state_html) { 'No changes' } + let(:diffs_html) { '' } before do - allow(controller_instance).to receive_messages(resource: mock_resource, response: response) - allow(controller_instance).to receive(:view_context) + allow(controller_instance).to receive_messages( + resource: mock_resource, + response: response, + rapid_diffs_enabled?: true, + view_context: nil, + stream_headers: nil, + params: ActionController::Parameters.new) + allow(controller_instance).to receive_message_chain(:helpers, :diff_view).and_return('inline') allow(mock_resource).to receive(:diffs_for_streaming).and_return(mock_diffs) + allow(RapidDiffs::DiffFileComponent).to receive_message_chain(:with_collection, :render_in) + .and_return(diffs_html) + end + + it 'renders diffs' do + controller_instance.send(:diffs) + expect(response.stream).to have_received(:write).with(diffs_html) end context 'when no diffs and no offset' do + let(:diff_files) { [] } + before do allow(controller_instance).to receive(:params).and_return(ActionController::Parameters.new) allow(RapidDiffs::EmptyStateComponent).to receive_message_chain(:new, :render_in).and_return(empty_state_html) end it 'renders empty state' do - controller_instance.send(:stream_diff_files, {}) - + controller_instance.send(:diffs) expect(response.stream).to have_received(:write).with(empty_state_html) end end @@ -85,10 +98,9 @@ RSpec.describe RapidDiffs::StreamingResource, type: :controller, feature_categor allow(controller_instance).to receive(:params).and_return(ActionController::Parameters.new(offset: '5')) end - it 'does not render empty state' do - expect(RapidDiffs::EmptyStateComponent).not_to receive(:new) - - controller_instance.send(:stream_diff_files, {}) + it 'renders diffs' do + controller_instance.send(:diffs) + expect(response.stream).to have_received(:write).with(diffs_html) end end end diff --git a/spec/db/migration_spec.rb b/spec/db/migration_spec.rb index 784f8753893..d7cc31cbd23 100644 --- a/spec/db/migration_spec.rb +++ b/spec/db/migration_spec.rb @@ -8,6 +8,7 @@ RSpec.describe 'Migrations Validation', feature_category: :database do # The range describes the timestamps that given migration helper can be used let(:all_migration_classes) do { + 2025_04_18_17_31_00.. => Gitlab::Database::Migration[2.3], 2023_10_10_02_15_00.. => Gitlab::Database::Migration[2.2], 2022_12_01_02_15_00..2023_11_01_02_15_00 => Gitlab::Database::Migration[2.1], 2022_01_26_21_06_58..2023_01_11_12_45_12 => Gitlab::Database::Migration[2.0], diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 34842602311..3fec44b4994 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do include Spec::Support::Helpers::ModalHelpers + include Features::WebIdeSpecHelpers let(:user) { create(:user) } let(:group) { create(:group, path: 'foo') } @@ -280,7 +281,7 @@ RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do expect(page).to have_current_path("/-/ide/project/#{group.readme_project.present.path_with_namespace}/edit/main/-/README.md/") - page.within('.ide') do + within_web_ide do expect(page).to have_text('README.md') end end @@ -300,7 +301,7 @@ RSpec.describe 'Edit group settings', feature_category: :groups_and_projects do expect(page).to have_current_path("/-/ide/project/#{group.full_path}/gitlab-profile/edit/main/-/README.md/") - page.within('.ide') do + within_web_ide do expect(page).to have_text('README.md') end end diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb index 615af4f5c5c..46d1ee96e12 100644 --- a/spec/features/ide_spec.rb +++ b/spec/features/ide_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_ide do include Features::WebIdeSpecHelpers - let_it_be(:ide_iframe_selector) { '#ide iframe' } let_it_be(:normal_project) { create(:project, :repository) } let(:project) { normal_project } @@ -21,22 +20,10 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id sign_in(user) end - shared_examples "legacy Web IDE" do - it 'loads legacy Web IDE', :aggregate_failures do - expect(page).to have_selector('.context-header', text: project.name) - - # Assert new Web IDE is not loaded - expect(page).not_to have_selector(ide_iframe_selector) - end - end - - shared_examples "new Web IDE" do - it 'loads new Web IDE', :aggregate_failures do - iframe = find(ide_iframe_selector) - - page.within_frame(iframe) do - expect(page).to have_selector('.title', text: project.path.upcase) - + shared_examples "Web IDE" do + it 'loads Web IDE', :aggregate_failures do + within_web_ide do + expect(page).to have_text(project.path.upcase) # Verify that the built-in GitLab Workflow Extension loads expect(page).to have_css('#GitLab\\.gitlab-workflow\\.gl\\.status\\.code_suggestions') end @@ -58,24 +45,12 @@ RSpec.describe 'IDE', :js, :with_current_organization, feature_category: :web_id let(:project) { subgroup_project } before do - stub_feature_flags(vscode_web_ide: true) stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates) ide_visit(project) end - it_behaves_like 'new Web IDE' - end - - describe 'with vscode feature flag off' do - before do - stub_feature_flags(vscode_web_ide: false) - stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates) - - ide_visit(project) - end - - it_behaves_like 'legacy Web IDE' + it_behaves_like 'Web IDE' end end end diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb index 5fe0f969b89..7f0177ec1a3 100644 --- a/spec/features/profiles/user_edit_preferences_spec.rb +++ b/spec/features/profiles/user_edit_preferences_spec.rb @@ -8,11 +8,9 @@ RSpec.describe 'User edit preferences profile', :js, feature_category: :user_pro # Empty value doesn't change the levels let(:language_percentage_levels) { nil } let(:user) { create(:user) } - let(:vscode_web_ide) { true } before do stub_languages_translation_percentage(language_percentage_levels) - stub_feature_flags(vscode_web_ide: vscode_web_ide) sign_in(user) visit(profile_preferences_path) end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index f73fcc774b4..522aea72e57 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -10,8 +10,6 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license let(:project_maintainer) { project.first_owner } before do - stub_feature_flags(vscode_web_ide: false) - sign_in(project_maintainer) end @@ -21,29 +19,8 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license expect(page).to have_current_path("/-/ide/project/#{project.full_path}/edit/master/-/LICENSE", ignore_query: true) - expect(page).to have_selector('[data-testid="file-templates-bar"]') - - select_template('MIT License') - - file_content = "Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}" - - expect(find('.monaco-editor')).to have_content('MIT License') - expect(find('.monaco-editor')).to have_content(file_content) - - ide_commit - - expect(page).to have_current_path("/-/ide/project/#{project.full_path}/tree/master/-/LICENSE/", ignore_query: true) - - expect(page).to have_content('All changes are committed') - - license_file = project.repository.blob_at('master', 'LICENSE').data - expect(license_file).to have_content('MIT License') - expect(license_file).to have_content(file_content) - end - - def select_template(template) - click_button 'Choose a template…' - click_button template - wait_for_requests + within_web_ide do + expect(page).to have_text('LICENSE') + end end end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 919a63e3ef7..0d5803e2729 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so include Features::SourceEditorSpecHelpers include ProjectForksHelper include Features::BlobSpecHelpers + include Features::WebIdeSpecHelpers include TreeHelper let_it_be(:json_text) { '{"name":"Best package ever!"}' } @@ -23,7 +24,6 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so let_it_be(:project_with_crlf) { create(:project, :custom_repo, name: 'Project with crlf', files: { 'crlf_file.txt' => crlf_text }) } before do - stub_feature_flags(vscode_web_ide: false) stub_feature_flags(blob_overflow_menu: false) sign_in(user) @@ -210,10 +210,10 @@ RSpec.describe 'Projects > Files > User edits files', :js, feature_category: :so click_link_or_button('Fork') - expect_fork_status - - expect(page).to have_css('.ide-sidebar-project-title', text: "#{project2.name} #{user.namespace.full_path}/#{project2.path}") - expect(page).to have_css('.ide .multi-file-tab', text: '.gitignore') + within_web_ide do + expect(page).to have_text(project2.path.upcase) + expect(page).to have_text('.gitignore') + end end it 'commits an edited file in a forked project', :sidekiq_might_not_need_inline do diff --git a/spec/features/projects/show/user_interacts_with_stars_spec.rb b/spec/features/projects/show/user_interacts_with_stars_spec.rb index 272c3dabb35..ecc13372329 100644 --- a/spec/features/projects/show/user_interacts_with_stars_spec.rb +++ b/spec/features/projects/show/user_interacts_with_stars_spec.rb @@ -9,7 +9,6 @@ RSpec.describe 'Projects > Show > User interacts with project stars', :js, featu let(:user) { create(:user) } before do - stub_feature_flags(vscode_web_ide: false) sign_in(user) visit(project_path(project)) end diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb index 430cd3449eb..a3ce675ea64 100644 --- a/spec/features/projects/show/user_manages_notifications_spec.rb +++ b/spec/features/projects/show/user_manages_notifications_spec.rb @@ -6,7 +6,6 @@ RSpec.describe 'Projects > Show > User manages notifications', :js, feature_cate let(:project) { create(:project, :public, :repository) } before do - stub_feature_flags(vscode_web_ide: false) sign_in(project.first_owner) end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb deleted file mode 100644 index e073a52f3b4..00000000000 --- a/spec/features/projects/tree/create_directory_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do - include Features::WebIdeSpecHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - where(:directory_code_dropdown_updates) do - [true, false] - end - - with_them do - before do - stub_feature_flags(vscode_web_ide: false) - stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates) - - project.add_maintainer(user) - sign_in(user) - - visit project_tree_path(project, :master) - - wait_for_requests - - ide_visit_from_link - end - - after do - set_cookie('new_repo', 'false') - end - - it 'creates directory in current directory' do - wait_for_all_requests - - all('.ide-tree-actions button').last.click - - page.within('.modal') do - find('.form-control').set('folder name') - - click_button('Create directory') - end - - expect(page).to have_content('folder name') - - first('.ide-tree-actions button').click - - page.within('.modal') do - find('.form-control').set('folder name/file name') - - click_button('Create file') - end - - wait_for_requests - - find('.js-ide-commit-mode').click - - # Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT, - # (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is - # taller (as it is by default with chrome headless) then the button will not exist. - if page.has_css?('[data-testid="begin-commit-button"]') - find_by_testid('begin-commit-button').click - end - - fill_in('commit-message', with: 'commit message ide') - - find(:css, ".js-ide-commit-new-mr input").set(false) - - wait_for_requests - - page.within '.multi-file-commit-form' do - click_button('Commit') - - wait_for_requests - end - - find('.js-ide-edit-mode').click - - expect(page).to have_content('folder name') - end - end -end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb deleted file mode 100644 index a731249cdb1..00000000000 --- a/spec/features/projects/tree/create_file_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Multi-file editor new file', :js, feature_category: :web_ide do - include Features::WebIdeSpecHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - where(:directory_code_dropdown_updates) do - [true, false] - end - - with_them do - before do - stub_feature_flags(vscode_web_ide: false) - stub_feature_flags(directory_code_dropdown_updates: directory_code_dropdown_updates) - - project.add_maintainer(user) - sign_in(user) - - visit project_path(project) - - wait_for_requests - - ide_visit_from_link - end - - after do - set_cookie('new_repo', 'false') - end - - it 'creates file in current directory' do - wait_for_all_requests - - first('.ide-tree-actions button').click - - page.within('.modal') do - find('.form-control').set('file name') - - click_button('Create file') - end - - wait_for_requests - - find('.js-ide-commit-mode').click - - # Compact mode depends on the size of window. If it is shorter than MAX_WINDOW_HEIGHT_COMPACT, - # (as it is with WEBDRIVER_HEADLESS=0), this initial commit button will exist. Otherwise, if it is - # taller (as it is by default with chrome headless) then the button will not exist. - if page.has_css?('[data-testid="begin-commit-button"]') - find_by_testid('begin-commit-button').click - end - - fill_in('commit-message', with: 'commit message ide') - - find(:css, ".js-ide-commit-new-mr input").set(false) - - page.within '.multi-file-commit-form' do - click_button('Commit') - - wait_for_requests - end - - find('.js-ide-edit-mode').click - - expect(page).to have_content('file name') - end - end -end diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb index e99666ea8be..5d011ae2757 100644 --- a/spec/features/projects/tree/tree_show_spec.rb +++ b/spec/features/projects/tree/tree_show_spec.rb @@ -135,40 +135,6 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do end end - context 'web IDE' do - before do - stub_feature_flags(vscode_web_ide: false) - end - - context 'when directory_code_dropdown_updates is enabled' do - it 'opens folder in IDE' do - stub_feature_flags(directory_code_dropdown_updates: true) - - visit project_tree_path(project, File.join('master', 'bar')) - ide_visit_from_link - - wait_for_all_requests - find('.ide-file-list') - wait_for_requests - expect(page).to have_selector('.is-open', text: 'bar') - end - end - - context 'when directory_code_dropdown_updates is disabled' do - it 'opens folder in IDE' do - stub_feature_flags(directory_code_dropdown_updates: false) - - visit project_tree_path(project, File.join('master', 'bar')) - ide_visit_from_link - - wait_for_all_requests - find('.ide-file-list') - wait_for_requests - expect(page).to have_selector('.is-open', text: 'bar') - end - end - end - context 'for subgroups' do let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb deleted file mode 100644 index 47139013b67..00000000000 --- a/spec/features/projects/tree/upload_file_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Multi-file editor upload file', :js, feature_category: :web_ide do - include Features::WebIdeSpecHelpers - - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') } - let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') } - - before do - stub_feature_flags(vscode_web_ide: false) - - project.add_maintainer(user) - sign_in(user) - - visit project_tree_path(project, :master) - - wait_for_requests - - ide_visit_from_link - end - - after do - set_cookie('new_repo', 'false') - end - - it 'uploads text file', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/415220' do - wait_for_all_requests - # make the field visible so capybara can use it - execute_script('document.querySelector("#file-upload").classList.remove("hidden")') - attach_file('file-upload', txt_file) - - expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt') - expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline).gsub!(/\s+/, ' ')) - end -end diff --git a/spec/features/work_items/shortcuts_work_item_spec.rb b/spec/features/work_items/shortcuts_work_item_spec.rb index 74701caa975..d97be6f90ef 100644 --- a/spec/features/work_items/shortcuts_work_item_spec.rb +++ b/spec/features/work_items/shortcuts_work_item_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Work item keyboard shortcuts', :js, feature_category: :team_planning do + include Features::WebIdeSpecHelpers + let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:work_item) { create(:work_item, project: project) } @@ -82,7 +84,9 @@ RSpec.describe 'Work item keyboard shortcuts', :js, feature_category: :team_plan new_tab = window_opened_by { find('body').native.send_key('.') } within_window new_tab do - expect(page).to have_selector('.ide-view') + within_web_ide do + expect(page).to have_text(project.path.upcase) + end end end end diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb index 4657e10ac1c..e3e5d19bb2e 100644 --- a/spec/finders/groups_finder_spec.rb +++ b/spec/finders/groups_finder_spec.rb @@ -487,6 +487,32 @@ RSpec.describe GroupsFinder, feature_category: :groups_and_projects do end end + context 'with active' do + let_it_be(:active_group) { create(:group, :public) } + let_it_be(:marked_for_deletion_group) { create(:group_with_deletion_schedule, :public) } + let_it_be(:archived_group) do + create(:group, :public, namespace_settings: create(:namespace_settings, archived: true)) + end + + subject { described_class.new(nil, params).execute.to_a } + + context 'when true' do + let(:params) { { active: true } } + + it 'returns active projects only' do + is_expected.to contain_exactly(active_group) + end + end + + context 'when false' do + let(:params) { { active: false } } + + it 'returns inactive projects only' do + is_expected.to contain_exactly(archived_group, marked_for_deletion_group) + end + end + end + describe 'group sorting' do let_it_be(:all_groups) { create_list(:group, 3, :public) } diff --git a/spec/frontend/ide/index_spec.js b/spec/frontend/ide/index_spec.js index 338cc2a6f97..d9027a4e9c8 100644 --- a/spec/frontend/ide/index_spec.js +++ b/spec/frontend/ide/index_spec.js @@ -2,7 +2,6 @@ import { startIde } from '~/ide/index'; import { IDE_ELEMENT_ID } from '~/ide/constants'; import { OAuthCallbackDomainMismatchErrorApp } from '~/ide/oauth_callback_domain_mismatch_error'; import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide'; -import { initLegacyWebIDE } from '~/ide/init_legacy_web_ide'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; @@ -13,7 +12,6 @@ const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect'; const MOCK_DATA_SET = { callbackUrls: JSON.stringify([`${TEST_HOST}/-/ide/oauth_redirect`]), - useNewWebIde: true, }; /** * @@ -43,7 +41,7 @@ describe('startIde', () => { document.getElementById(IDE_ELEMENT_ID)?.remove(); }); - describe('when useNewWebIde feature flag is true', () => { + describe('default', () => { let ideElement; beforeEach(async () => { @@ -66,7 +64,6 @@ describe('startIde', () => { it('renders error page', async () => { setupMockIdeElement({ callbackUrls: JSON.stringify([MOCK_MISMATCH_CALLBACK_URL]), - useNewWebIde: true, }); await startIde(); @@ -84,7 +81,6 @@ describe('startIde', () => { callbackUrls: JSON.stringify([ `${parsedUrl.protocol}//${parsedUrl.host.toUpperCase()}/-/ide/oauth_redirect`, ]), - useNewWebIde: true, }); await startIde(); @@ -98,7 +94,6 @@ describe('startIde', () => { it('renders error page', async () => { setupMockIdeElement({ callbackUrls: JSON.stringify(['/-/ide/oauth_redirect']), - useNewWebIde: true, }); await startIde(); @@ -120,21 +115,4 @@ describe('startIde', () => { expect(initGitlabWebIDE).not.toHaveBeenCalled(); }); }); - - describe('when useNewWebIde feature flag is false', () => { - beforeEach(async () => { - setupMockIdeElement({ useNewWebIde: false }); - - await startIde(); - }); - - it('calls initGitlabWebIDE', () => { - expect(initLegacyWebIDE).toHaveBeenCalledTimes(1); - expect(initGitlabWebIDE).toHaveBeenCalledTimes(0); - }); - - it('does not render error page', () => { - expect(renderErrorSpy).not.toHaveBeenCalled(); - }); - }); }); diff --git a/spec/frontend/work_items/components/create_work_item_spec.js b/spec/frontend/work_items/components/create_work_item_spec.js index ced9fce487a..c68ecb44ddb 100644 --- a/spec/frontend/work_items/components/create_work_item_spec.js +++ b/spec/frontend/work_items/components/create_work_item_spec.js @@ -64,7 +64,7 @@ describe('Create work item component', () => { const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse); const mutationErrorHandler = jest.fn().mockResolvedValue(createWorkItemMutationErrorResponse); const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); - const workItemQuerySuccessHandler = jest.fn().mockResolvedValue(createWorkItemQueryResponse); + const workItemQuerySuccessHandler = jest.fn().mockResolvedValue(createWorkItemQueryResponse()); const namespaceWorkItemTypesHandler = jest .fn() .mockResolvedValue(namespaceWorkItemTypesQueryResponse); diff --git a/spec/frontend/work_items/graphql/resolvers_spec.js b/spec/frontend/work_items/graphql/resolvers_spec.js index 9819c882f2a..4edc644fd35 100644 --- a/spec/frontend/work_items/graphql/resolvers_spec.js +++ b/spec/frontend/work_items/graphql/resolvers_spec.js @@ -55,7 +55,7 @@ describe('work items graphql resolvers', () => { mockApollo.clients.defaultClient.cache.writeQuery({ query: workItemByIidQuery, variables: { fullPath: fullPathWithId, iid }, - data: createWorkItemQueryResponse.data, + data: createWorkItemQueryResponse().data, }); mockApolloClient = mockApollo.clients.defaultClient; }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 94dc82c9d03..5002abb6cf0 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -5803,7 +5803,7 @@ export const namespaceGroupsList = { }, }; -export const createWorkItemQueryResponse = { +export const createWorkItemQueryResponse = (widgets = []) => ({ data: { workspace: { id: 'full-path-epic-id', @@ -6013,14 +6013,14 @@ export const createWorkItemQueryResponse = { }, __typename: 'WorkItemWidgetWeight', }, - customFieldsWidgetResponseFactory(), + ...widgets, ], __typename: 'WorkItem', }, __typename: 'Namespace', }, }, -}; +}); export const mockToggleResolveDiscussionResponse = { data: { diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb index 8d1c9a6bd87..c146a413053 100644 --- a/spec/graphql/resolvers/users/participants_resolver_spec.rb +++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb @@ -137,10 +137,9 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do # 1 extra query per source (3 emojis + 2 notes) to fetch participables collection # 2 extra queries to load work item widgets collection - # 1 extra query for root_ancestor in custom_fields_feature feature flag check # 1 extra query to load the project creator to check if they are banned # 1 extra query to load the invited groups to see if the user is banned from any of them - expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(10) + expect { query.call }.not_to exceed_query_limit(control_count).with_threshold(9) end it 'does not execute N+1 for system note metadata relation' do diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index 207c344151d..b1401a3f81b 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -32,11 +32,12 @@ RSpec.describe IdeHelper, feature_category: :web_ide do let(:base_data) do { - 'use-new-web-ide' => 'false', 'user-preferences-path' => profile_preferences_path, 'sign-in-path' => 'test-sign-in-path', - 'project' => nil, - 'preview-markdown-path' => nil + 'project-path' => nil, + 'new-web-ide-help-page-path' => + help_page_path('user/project/web_ide/_index.md'), + 'csp-nonce' => 'test-csp-nonce' } end @@ -47,8 +48,6 @@ RSpec.describe IdeHelper, feature_category: :web_ide do context 'with project' do it 'returns hash with parameters' do - serialized_project = API::Entities::Project.represent(project, current_user: user).to_json - expect( helper.ide_data(project: project, fork_info: nil, params: params) ).to include(base_data.merge( @@ -56,8 +55,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do 'branch-name' => params[:branch], 'file-path' => params[:path], 'merge-request' => params[:merge_request_id], - 'project' => serialized_project, - 'preview-markdown-path' => Gitlab::Routing.url_helpers.project_preview_markdown_path(project) + 'project-path' => project.full_path )) end @@ -69,93 +67,51 @@ RSpec.describe IdeHelper, feature_category: :web_ide do end end - context 'with vscode_web_ide=true' do - let(:base_data) do - { - 'use-new-web-ide' => 'true', - 'user-preferences-path' => profile_preferences_path, - 'sign-in-path' => 'test-sign-in-path', - 'new-web-ide-help-page-path' => - help_page_path('user/project/web_ide/_index.md'), - 'csp-nonce' => 'test-csp-nonce' - } + it 'includes editor font configuration' do + ide_data = helper.ide_data(project: nil, fork_info: fork_info, params: params) + editor_font = ::Gitlab::Json.parse(ide_data.fetch('editor-font'), symbolize_names: true) + + expect(editor_font).to include({ + fallback_font_family: 'monospace', + font_faces: [ + { + family: 'GitLab Mono', + display: 'block', + src: [{ + url: a_string_matching(%r{gitlab-mono/GitLabMono-[^I]}), + format: 'woff2' + }] + }, + { + family: 'GitLab Mono', + display: 'block', + style: 'italic', + src: [{ + url: a_string_matching(%r{gitlab-mono/GitLabMono-Italic}), + format: 'woff2' + }] + } + ] + }) + end + + context 'for extension marketplace data' do + where(:settings, :expected_settings_hash) do + ref(:disabled_vscode_settings) | nil + ref(:enabled_vscode_settings) | 'c6620244fe72864fa8d8' end - before do - stub_feature_flags(vscode_web_ide: true) - end + with_them do + it 'includes extension marketplace settings and settings context hash' do + expect(WebIde::ExtensionMarketplace).to receive(:webide_extension_marketplace_settings) + .with(user: user).and_return(settings) - it 'returns hash' do - expect(helper.ide_data(project: nil, fork_info: fork_info, params: params)) - .to include(base_data) - end + actual = helper.ide_data(project: nil, fork_info: fork_info, params: params) - it 'includes editor font configuration' do - ide_data = helper.ide_data(project: nil, fork_info: fork_info, params: params) - editor_font = ::Gitlab::Json.parse(ide_data.fetch('editor-font'), symbolize_names: true) - - expect(editor_font).to include({ - fallback_font_family: 'monospace', - font_faces: [ - { - family: 'GitLab Mono', - display: 'block', - src: [{ - url: a_string_matching(%r{gitlab-mono/GitLabMono-[^I]}), - format: 'woff2' - }] - }, - { - family: 'GitLab Mono', - display: 'block', - style: 'italic', - src: [{ - url: a_string_matching(%r{gitlab-mono/GitLabMono-Italic}), - format: 'woff2' - }] - } - ] - }) - end - - it 'does not use new web ide if feature flag is disabled' do - stub_feature_flags(vscode_web_ide: false) - - expect(helper.ide_data(project: nil, fork_info: fork_info, params: params)) - .to include('use-new-web-ide' => 'false') - end - - context 'for extension marketplace data' do - where(:settings, :expected_settings_hash) do - ref(:disabled_vscode_settings) | nil - ref(:enabled_vscode_settings) | 'c6620244fe72864fa8d8' - end - - with_them do - it 'includes extension marketplace settings and settings context hash' do - expect(WebIde::ExtensionMarketplace).to receive(:webide_extension_marketplace_settings) - .with(user: user).and_return(settings) - - actual = helper.ide_data(project: nil, fork_info: fork_info, params: params) - - expect(actual).to include({ - 'extension-marketplace-settings' => settings.to_json, - 'settings-context-hash' => expected_settings_hash - }) - end - end - end - - context 'with project' do - it 'returns hash with parameters' do - expect( - helper.ide_data(project: project, fork_info: nil, params: params) - ).to include(base_data.merge( - 'branch-name' => params[:branch], - 'file-path' => params[:path], - 'merge-request' => params[:merge_request_id], - 'fork-info' => nil - )) + expect(actual).to include({ + 'extension-marketplace-settings' => settings.to_json, + 'settings-context-hash' => expected_settings_hash + }) end end end @@ -164,10 +120,6 @@ RSpec.describe IdeHelper, feature_category: :web_ide do describe '#show_web_ide_oauth_callback_mismatch_callout?' do let_it_be(:oauth_application) { create(:oauth_application, owner: nil) } - before do - stub_feature_flags(vscode_web_ide: true) - end - it 'returns false if no Web IDE OAuth application found' do expect(helper.show_web_ide_oauth_callback_mismatch_callout?).to be false end diff --git a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt index 32f6b1843be..7e1f9c808d6 100644 --- a/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt +++ b/spec/lib/generators/batched_background_migration/expected_files/queue_my_batched_migration.txt @@ -5,7 +5,7 @@ # Update below commented lines with appropriate values. -class QueueMyBatchedMigration < Gitlab::Database::Migration[2.2] +class QueueMyBatchedMigration < Gitlab::Database::Migration[2.3] milestone '16.6' # Select the applicable gitlab schema for your batched background migration diff --git a/spec/lib/generators/model/mocks/migration_file.txt b/spec/lib/generators/model/mocks/migration_file.txt index d0a5c71ffc3..0681fb287fc 100644 --- a/spec/lib/generators/model/mocks/migration_file.txt +++ b/spec/lib/generators/model/mocks/migration_file.txt @@ -3,7 +3,7 @@ # See https://docs.gitlab.com/ee/development/migration_style_guide.html # for more information on how to write migrations for GitLab. -class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.2] +class CreateModelGeneratorTestFoos < Gitlab::Database::Migration[2.3] # When using the methods "add_concurrent_index" or "remove_concurrent_index" # you must disable the use of transactions # as these methods can not run in an existing transaction. diff --git a/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb b/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb index 035220e8407..b01040adadd 100644 --- a/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb +++ b/spec/lib/gitlab/background_migration/encrypt_ci_trigger_token_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::BackgroundMigration::EncryptCiTriggerToken, feature_categ ci_trigger.send :attr_encrypted, :encrypted_token_tmp, attribute: :encrypted_token, mode: :per_attribute_iv, - key: ::Settings.attr_encrypted_db_key_base_32, + key: Settings.attr_encrypted_db_key_base_32, algorithm: 'aes-256-gcm', encode: false end diff --git a/spec/lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks_spec.rb b/spec/lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks_spec.rb new file mode 100644 index 00000000000..c17533dab0e --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers/require_disable_ddl_transaction_for_multiple_locks_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::MigrationHelpers::RequireDisableDdlTransactionForMultipleLocks, + query_analyzers: false, feature_category: :database do + let_it_be(:multiple_locks_migration_module) do + Module.new do + include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers + + def partitioned_table + :ci_runner_machines + end + + def tables + %i[ + instance_type_ci_runner_machines group_type_ci_runner_machines project_type_ci_runner_machines + ci_runner_machines + ].freeze + end + + def up + drop_trigger(:ci_runner_machines_archived, :ci_runner_machines_loose_fk_trigger) + + tables.each do |table| + untrack_record_deletions(table) + + track_record_deletions_override_table_name(table, partitioned_table) + end + end + + def down + tables.each do |table| + untrack_record_deletions(table) + end + + execute(<<~SQL.squish) + CREATE TRIGGER #{record_deletion_trigger_name(partitioned_table)} + AFTER DELETE ON ci_runner_machines_archived REFERENCING OLD TABLE AS old_table + FOR EACH STATEMENT + EXECUTE FUNCTION #{INSERT_FUNCTION_NAME}(); + SQL + end + end + end + + let_it_be(:multiple_locks_single_statement_migration_module) do + Module.new do + def up + # Simulate an operation that locks multiple tables + execute "LOCK TABLE ci_runners, ci_runner_machines IN ACCESS EXCLUSIVE MODE" + end + + def down; end + end + end + + let(:migration_base_klass) do + Class.new(Gitlab::Database::Migration[migration_version]) + end + + before do + stub_const('TestMultipleLocksMigrationModule', multiple_locks_migration_module) + stub_const('TestMultipleLocksInSingleStmtMigrationModule', multiple_locks_single_statement_migration_module) + + migration.instance_variable_set(:@_defining_file, 'db/migrate/00000000000000_example.rb') + migration.milestone '18.0' + end + + context 'when executing migrations' do + subject(:migrate_up) { migration.migrate(:up) } + + let(:migration_version) { 2.3 } + + context 'when migration locks multiple tables in single statement' do + let(:migration) { migration_base_klass.extend(TestMultipleLocksInSingleStmtMigrationModule) } + + it 'does not raise an error' do + expect { migrate_up }.not_to raise_error + end + end + + context 'when migration locks multiple tables across multiple statements' do + let(:migration) { migration_base_klass.extend(TestMultipleLocksMigrationModule) } + + context 'when migration does not include module' do + let(:migration_version) { 2.2 } + + it 'does not raise an error' do + expect { migrate_up }.not_to raise_error + end + end + + context 'when migration includes module' do + let(:migration_version) { 2.3 } + + it 'fails with error about multiple locks' do + expect do + migration.migrate(:up) + end.to raise_error(/This migration locks multiple tables across different statements/) + end + + context 'when migration disables ddl transaction' do + let(:migration) do + Class.new(Gitlab::Database::Migration[migration_version]) do + disable_ddl_transaction! + + extend TestMultipleLocksMigrationModule + end + end + + it 'does not raise an error' do + expect { migrate_up }.not_to raise_error + end + end + + context 'when migration skips check' do + let(:migration) do + Class.new(Gitlab::Database::Migration[migration_version]) do + skip_require_disable_ddl_transactions! + + extend TestMultipleLocksMigrationModule + end + end + + it 'does not raise an error' do + expect { migrate_up }.not_to raise_error + end + end + end + end + end +end diff --git a/spec/lib/web_ide/default_oauth_application_spec.rb b/spec/lib/web_ide/default_oauth_application_spec.rb index 3d67ab7f5c7..146b3988d67 100644 --- a/spec/lib/web_ide/default_oauth_application_spec.rb +++ b/spec/lib/web_ide/default_oauth_application_spec.rb @@ -6,23 +6,6 @@ RSpec.describe WebIde::DefaultOauthApplication, feature_category: :web_ide do let_it_be(:current_user) { create(:user) } let_it_be(:oauth_application) { create(:oauth_application, owner: nil) } - describe '#feature_enabled?' do - where(:vscode_web_ide, :expectation) do - [ - [ref(:current_user), true], - [false, false] - ] - end - - with_them do - it 'returns the expected value' do - stub_feature_flags(vscode_web_ide: vscode_web_ide) - - expect(described_class.feature_enabled?(current_user)).to be(expectation) - end - end - end - describe '#oauth_application' do it 'returns web_ide_oauth_application from application_settings' do expect(described_class.oauth_application).to be_nil diff --git a/spec/lib/web_ide/extension_marketplace_spec.rb b/spec/lib/web_ide/extension_marketplace_spec.rb index 7d542d4c072..19f69a3926b 100644 --- a/spec/lib/web_ide/extension_marketplace_spec.rb +++ b/spec/lib/web_ide/extension_marketplace_spec.rb @@ -25,19 +25,17 @@ RSpec.describe WebIde::ExtensionMarketplace, feature_category: :web_ide do let_it_be_with_reload(:current_user) { create(:user) } describe 'feature enabled methods' do - where(:vscode_web_ide, :web_ide_extensions_marketplace, :vscode_extension_marketplace_settings, :app_setting, + where(:web_ide_extensions_marketplace, :vscode_extension_marketplace_settings, :app_setting, :expectation) do - ref(:current_user) | ref(:current_user) | false | {} | true - ref(:current_user) | ref(:current_user) | true | {} | false - ref(:current_user) | ref(:current_user) | true | { enabled: true } | true - ref(:current_user) | false | false | { enabled: true } | false - false | ref(:current_user) | false | {} | false + ref(:current_user) | false | {} | true + ref(:current_user) | true | {} | false + ref(:current_user) | true | { enabled: true } | true + false | false | { enabled: true } | false end with_them do before do stub_feature_flags( - vscode_web_ide: vscode_web_ide, web_ide_extensions_marketplace: web_ide_extensions_marketplace, vscode_extension_marketplace_settings: vscode_extension_marketplace_settings ) @@ -129,8 +127,7 @@ RSpec.describe WebIde::ExtensionMarketplace, feature_category: :web_ide do before do stub_feature_flags( vscode_extension_marketplace_settings: vscode_extension_marketplace_settings, - web_ide_extensions_marketplace: web_ide_extensions_marketplace, - vscode_web_ide: true + web_ide_extensions_marketplace: web_ide_extensions_marketplace ) stub_application_setting(vscode_extension_marketplace: app_setting) diff --git a/spec/migrations/db/post_migrate/20250422093734_outdate_namespace_descendants_cache_spec.rb b/spec/migrations/db/post_migrate/20250422093734_outdate_namespace_descendants_cache_spec.rb new file mode 100644 index 00000000000..4ca08d60c47 --- /dev/null +++ b/spec/migrations/db/post_migrate/20250422093734_outdate_namespace_descendants_cache_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe OutdateNamespaceDescendantsCache, migration: :gitlab_main, feature_category: :database do + let(:migration) { described_class.new } + + let(:namespace_descendants) { table(:namespace_descendants) } + + let(:outdated_at) { Date.new(2022, 1, 1) } + let!(:ns_outdated) { namespace_descendants.create!(namespace_id: 1, outdated_at: outdated_at) } + let!(:ns_up_to_date) { namespace_descendants.create!(namespace_id: 2) } + + describe '#up' do + it 'bumps all timestamp values' do + migrate! + + records = namespace_descendants.all + outdated_ats = records.map(&:outdated_at) + expect(outdated_ats).to all(be_present) + end + end +end diff --git a/spec/migrations/generate_ci_job_token_signing_key_spec.rb b/spec/migrations/generate_ci_job_token_signing_key_spec.rb index b260ddea74b..60e900baa68 100644 --- a/spec/migrations/generate_ci_job_token_signing_key_spec.rb +++ b/spec/migrations/generate_ci_job_token_signing_key_spec.rb @@ -6,11 +6,13 @@ require_migration! RSpec.describe GenerateCiJobTokenSigningKey, feature_category: :continuous_integration do let(:application_settings) do Class.new(ActiveRecord::Base) do + include Gitlab::EncryptedAttribute + self.table_name = 'application_settings' attr_encrypted :ci_job_token_signing_key, { mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/spec/migrations/regenerate_ci_job_token_signing_key_spec.rb b/spec/migrations/regenerate_ci_job_token_signing_key_spec.rb index 73acdabdd71..25e75d9052e 100644 --- a/spec/migrations/regenerate_ci_job_token_signing_key_spec.rb +++ b/spec/migrations/regenerate_ci_job_token_signing_key_spec.rb @@ -6,11 +6,13 @@ require_migration! RSpec.describe RegenerateCiJobTokenSigningKey, feature_category: :continuous_integration do let(:application_settings) do Class.new(ActiveRecord::Base) do + include Gitlab::EncryptedAttribute + self.table_name = 'application_settings' attr_encrypted :ci_job_token_signing_key, { mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, algorithm: 'aes-256-gcm', encode: false, encode_iv: false diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb index 5ac446fc56a..e60789c6253 100644 --- a/spec/models/concerns/bulk_insert_safe_spec.rb +++ b/spec/models/concerns/bulk_insert_safe_spec.rb @@ -69,6 +69,7 @@ RSpec.describe BulkInsertSafe, feature_category: :database do include BulkInsertSafe include ShaAttribute + include Gitlab::EncryptedAttribute validates :name, :enum_value, :secret_value, :sha_value, :jsonb_value, presence: true @@ -81,7 +82,7 @@ RSpec.describe BulkInsertSafe, feature_category: :database do attr_encrypted :secret_value, mode: :per_attribute_iv, algorithm: 'aes-256-gcm', - key: Settings.attr_encrypted_db_key_base_32, + key: :db_key_base_32, insecure_mode: false attribute :enum_value, default: 'case_1' diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index a25a1136241..0ec6e60c3dc 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1643,6 +1643,30 @@ RSpec.describe Group, feature_category: :groups_and_projects do it { is_expected.to match_array([subgroup, subsubgroup]) } end end + + describe '.active' do + let_it_be(:active_group) { create(:group) } + let_it_be(:archived_group) { create(:group, namespace_settings: create(:namespace_settings, archived: true)) } + let_it_be(:marked_for_deletion_group) { create(:group_with_deletion_schedule) } + + subject { described_class.active } + + it { is_expected.to include(active_group) } + it { is_expected.not_to include(marked_for_deletion_group) } + it { is_expected.not_to include(archived_group) } + end + + describe '.inactive' do + let_it_be(:active_group) { create(:group) } + let_it_be(:archived_group) { create(:group, namespace_settings: create(:namespace_settings, archived: true)) } + let_it_be(:marked_for_deletion_group) { create(:group_with_deletion_schedule) } + + subject { described_class.inactive } + + it { is_expected.to include(archived_group) } + it { is_expected.to include(marked_for_deletion_group) } + it { is_expected.not_to include(active_group) } + end end describe '.project_creation_levels_for_user' do diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb index 295d8714aba..333af52af12 100644 --- a/spec/models/integration_spec.rb +++ b/spec/models/integration_spec.rb @@ -1446,10 +1446,12 @@ RSpec.describe Integration, feature_category: :integrations do expect(hash['encrypted_properties']).not_to eq(record.encrypted_properties) expect(hash['encrypted_properties_iv']).not_to eq(record.encrypted_properties_iv) + options = record.send(:evaluated_attr_encrypted_options_for, :properties) + .merge(iv: hash['encrypted_properties_iv']) decrypted = described_class.attr_decrypt( :properties, hash['encrypted_properties'], - { iv: hash['encrypted_properties_iv'] } + options ) expect(decrypted).to eq db_props diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 8f0aa706182..3796b2ba3a4 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -100,6 +100,19 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group end end + shared_examples 'API that accepts active parameter for GroupFinder' do + [nil, true, false].each do |active_value| + it "passes #{active_value.inspect} active parameter to the GroupsFinder" do + params = { active: active_value } + + expect(GroupsFinder).to receive(:new) + .with(user1, hash_including(active: active_value)).and_call_original + + get api('/groups', user1), params: params + end + end + end + describe "GET /groups" do shared_examples 'groups list N+1' do it 'avoids N+1 queries', :use_sql_query_cache do @@ -602,6 +615,8 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group expect(response_groups).to match_array([group1.id, subgroup1.id]) end end + + it_behaves_like 'API that accepts active parameter for GroupFinder' end describe "GET /groups/:id" do @@ -2816,6 +2831,8 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group let(:parent) { group1 } let(:endpoint) { api("/groups/#{group1.id}/subgroups", user1) } end + + it_behaves_like 'API that accepts active parameter for GroupFinder' end describe 'GET /groups/:id/descendant_groups' do @@ -2959,6 +2976,8 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group let(:parent) { group1 } let(:endpoint) { api("/groups/#{group1.id}/descendant_groups", user1) } end + + it_behaves_like 'API that accepts active parameter for GroupFinder' end describe "POST /groups" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a71cc79c2b8..840a6158856 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -279,6 +279,38 @@ RSpec.describe API::Projects, :aggregate_failures, feature_category: :groups_and end end + context 'when filtering by active parameter' do + let_it_be(:marked_for_deletion_project) do + create(:project, marked_for_deletion_on: Date.parse('2024-01-01'), namespace: user.namespace) + end + + let_it_be(:archived_project) do + create(:project, :archived, namespace: user.namespace) + end + + context 'when active is true' do + it 'returns only non archived and not marked for deletion projects' do + get api(path, user), params: { active: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).not_to include(archived_project.id, marked_for_deletion_project.id) + end + end + + context 'when active is false' do + it 'returns only archived or marked for deletion projects' do + get api(path, user), params: { active: false } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(archived_project.id, marked_for_deletion_project.id) + end + end + end + shared_examples 'includes container_registry_access_level' do specify do project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index 50f0393fa46..f2a45140b86 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -20,7 +20,6 @@ RSpec.describe IdeController, feature_category: :web_ide do let(:user) { creator } before do - stub_feature_flags(vscode_web_ide: true) sign_in(user) end @@ -147,36 +146,10 @@ RSpec.describe IdeController, feature_category: :web_ide do end end - describe 'legacy Web IDE' do - before do - stub_feature_flags(vscode_web_ide: false) - end + it 'uses fullscreen layout' do + subject - it 'uses application layout' do - subject - - expect(response).to render_template('layouts/application') - end - - it 'does not create oauth application' do - expect(Doorkeeper::Application).not_to receive(:new) - - subject - - expect(web_ide_oauth_application).to be_nil - end - end - - describe 'vscode IDE' do - before do - stub_feature_flags(vscode_web_ide: true) - end - - it 'uses fullscreen layout' do - subject - - expect(response).to render_template('layouts/fullscreen') - end + expect(response).to render_template('layouts/fullscreen') end it 'ensures web_ide_oauth_application' do @@ -242,14 +215,6 @@ RSpec.describe IdeController, feature_category: :web_ide do expect(response).to have_gitlab_http_status(:ok) end - - it 'with vscode_web_ide flag off, returns not_found' do - stub_feature_flags(vscode_web_ide: false) - - oauth_redirect - - expect(response).to have_gitlab_http_status(:not_found) - end end end diff --git a/spec/requests/projects/merge_requests/diffs_stream_spec.rb b/spec/requests/projects/merge_requests/diffs_stream_spec.rb index d722ff99202..ad57117bc66 100644 --- a/spec/requests/projects/merge_requests/diffs_stream_spec.rb +++ b/spec/requests/projects/merge_requests/diffs_stream_spec.rb @@ -44,8 +44,10 @@ RSpec.describe 'Merge Requests Diffs stream', feature_category: :code_review_wor context 'when accessed' do it 'passes hash of options to #diffs_for_streaming' do expect_next_instance_of(::Projects::MergeRequests::DiffsStreamController) do |controller| + context = {} + allow(controller).to receive(:view_context).and_return(context) expect(controller).to receive(:stream_diff_files) - .with(diff_options_hash) + .with(diff_options_hash, context) .and_call_original end diff --git a/spec/rubocop/cop/gitlab/rails/attr_encrypted_spec.rb b/spec/rubocop/cop/gitlab/rails/attr_encrypted_spec.rb index 53ac1358272..262aaf425ec 100644 --- a/spec/rubocop/cop/gitlab/rails/attr_encrypted_spec.rb +++ b/spec/rubocop/cop/gitlab/rails/attr_encrypted_spec.rb @@ -15,7 +15,7 @@ RSpec.describe RuboCop::Cop::Gitlab::Rails::AttrEncrypted, feature_category: :sh ^^^^^^^^^^^^^^^^^^^^^^^ Use `encrypts` over deprecated `attr_encrypted` to encrypt a column. See https://docs.gitlab.com/development/migration_style_guide/#encrypted-attributes mode: :per_attribute_iv_and_salt, insecure_mode: true, - key: Settings.attr_encrypted_db_key_base, + key: :db_key_base, algorithm: 'aes-256-cbc' end RUBY diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a68c01c23d8..dc407b9d462 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -310,10 +310,6 @@ RSpec.configure do |config| # cause spec failures. stub_feature_flags(gitlab_error_tracking: false) - # Disable this to avoid the Web IDE modals popping up in tests: - # https://gitlab.com/gitlab-org/gitlab/-/issues/385453 - stub_feature_flags(vscode_web_ide: false) - # Disable `main_branch_over_master` as we migrate # from `master` to `main` accross our codebase. # It's done in order to preserve the concistency in tests diff --git a/spec/support/helpers/features/web_ide_spec_helpers.rb b/spec/support/helpers/features/web_ide_spec_helpers.rb index 32b27864e0b..0ad8abb966f 100644 --- a/spec/support/helpers/features/web_ide_spec_helpers.rb +++ b/spec/support/helpers/features/web_ide_spec_helpers.rb @@ -30,97 +30,9 @@ module Features switch_to_window new_tab end - def ide_tree_body - page.find('.ide-tree-body') - end - - def ide_tree_actions - page.find('.ide-tree-actions') - end - - def ide_tab_selector(mode) - ".js-ide-#{mode}-mode" - end - - def ide_folder_row_open?(row) - row.matches_css?('.folder.is-open') - end - - # Deletes a file by traversing to `path` - # then clicking the 'Delete' action. - # - # - Throws an error if the file is not found - def ide_delete_file(path) - container = ide_traverse_to_file(path) - - click_file_action(container, 'Delete') - end - - # Opens parent directories until the file at `path` - # is exposed. - # - # - Returns a reference to the file row at `path` - # - Throws an error if the file is not found - def ide_traverse_to_file(path) - paths = path.split('/') - container = nil - - paths.each_with_index do |path, index| - ide_open_file_row(container) if container - container = find_file_child(container, path, level: index) - end - - container - end - - def ide_open_file_row(row) - return if ide_folder_row_open?(row) - - row.click - end - - def ide_set_editor_value(value) - editor_set_value(value) - end - - def ide_commit_tab_selector - ide_tab_selector('commit') - end - - def ide_commit - find(ide_commit_tab_selector).click - - commit_to_current_branch - end - - private - - def file_row_container(row) - row ? row.find(:xpath, '..') : ide_tree_body - end - - def find_file_child(row, name, level: nil) - container = file_row_container(row) - container.find(".file-row[data-level=\"#{level}\"]", text: name) - end - - def click_file_action(row, text) - row.hover - dropdown = row.find('.ide-new-btn') - dropdown.find('button').click - dropdown.find('button', text: text).click - end - - def commit_to_current_branch(option: 'Commit to master branch', message: '') - within '.multi-file-commit-form' do - fill_in('commit-message', with: message) if message - - choose(option) - - click_button('Commit') - - wait_for_requests - end + def within_web_ide(&block) + iframe = find('#ide iframe') + page.within_frame(iframe, &block) end end end diff --git a/spec/support/helpers/user_with_namespace_shim.yml b/spec/support/helpers/user_with_namespace_shim.yml index a9cd4b4fa4a..d02bf6e861d 100644 --- a/spec/support/helpers/user_with_namespace_shim.yml +++ b/spec/support/helpers/user_with_namespace_shim.yml @@ -547,8 +547,6 @@ - spec/features/projects/sourcegraph_csp_spec.rb - spec/features/projects/tags/user_views_tag_spec.rb - spec/features/projects/tags/user_views_tags_spec.rb -- spec/features/projects/tree/create_directory_spec.rb -- spec/features/projects/tree/create_file_spec.rb - spec/features/projects/tree/rss_spec.rb - spec/features/projects/tree/tree_show_spec.rb - spec/features/projects/user_creates_project_spec.rb diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 1c22dbf52cb..88690239c6b 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -110,7 +110,6 @@ - './ee/spec/features/groups_spec.rb' - './ee/spec/features/groups/wikis_spec.rb' - './ee/spec/features/groups/wiki/user_views_wiki_empty_spec.rb' -- './ee/spec/features/ide/user_opens_ide_spec.rb' - './ee/spec/features/integrations/jira/jira_issues_list_spec.rb' - './ee/spec/features/issues/blocking_issues_spec.rb' - './ee/spec/features/issues/epic_in_issue_sidebar_spec.rb' @@ -3272,11 +3271,8 @@ - './spec/features/projects/tags/user_views_tag_spec.rb' - './spec/features/projects/tags/user_views_tags_spec.rb' - './spec/features/projects/terraform_spec.rb' -- './spec/features/projects/tree/create_directory_spec.rb' -- './spec/features/projects/tree/create_file_spec.rb' - './spec/features/projects/tree/rss_spec.rb' - './spec/features/projects/tree/tree_show_spec.rb' -- './spec/features/projects/tree/upload_file_spec.rb' - './spec/features/projects/user_changes_project_visibility_spec.rb' - './spec/features/projects/user_creates_project_spec.rb' - './spec/features/projects/user_sees_sidebar_spec.rb' diff --git a/spec/tooling/ci/changed_files_spec.rb b/spec/tooling/ci/changed_files_spec.rb index b9dc41f4161..31247772c6f 100644 --- a/spec/tooling/ci/changed_files_spec.rb +++ b/spec/tooling/ci/changed_files_spec.rb @@ -7,7 +7,7 @@ RSpec.describe CI::ChangedFiles, feature_category: :tooling do let(:instance) { described_class.new } describe '#get_changed_files_in_merged_results_pipeline' do - let(:git_diff_output) { "file1.js\nfile2.rb\nfile3.vue" } + let(:git_diff_output) { "file1.js\nfile2.rb\nfile3.vue\nfile4.graphql" } before do allow(instance).to receive(:`) @@ -18,7 +18,7 @@ RSpec.describe CI::ChangedFiles, feature_category: :tooling do context 'when git diff is run in a merged results pipeline' do it 'returns an array when there are changed files' do expect(instance.get_changed_files_in_merged_results_pipeline) - .to match_array(['file1.js', 'file2.rb', 'file3.vue']) + .to match_array(['file1.js', 'file2.rb', 'file3.vue', 'file4.graphql']) end context "when there are no changed files" do @@ -117,6 +117,175 @@ RSpec.describe CI::ChangedFiles, feature_category: :tooling do expect(instance.run_eslint_for_changed_files).to eq(0) end end + + context 'when a single todo file has been changed' do + let(:eslint_command) do + ['yarn', 'run', 'lint:eslint', '--no-warn-ignored', '--format', 'gitlab', + '.eslint_todo/vue-no-unused-properties.mjs', + 'app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue', + 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue', + 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue', + 'app/assets/javascripts/admin/statistics_panel/components/app.vue', + 'app/assets/javascripts/badges/components/badge.vue', + 'app/assets/javascripts/badges/components/badge_form.vue', + 'app/assets/javascripts/batch_comments/components/draft_note.vue'] + end + + let(:files) { ['.eslint_todo/vue-no-unused-properties.mjs'] } + let(:git_diff_output) do + <<-DIFF + diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs + index 81f0b2cbcf84..ef936567f2e8 100644 + --- a/.eslint_todo/vue-no-unused-properties.mjs + +++ b/.eslint_todo/vue-no-unused-properties.mjs + @@ -3,13 +3,7 @@ + */ + export default { + files: [ + - 'app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue', + - 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue', + - 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue', + - 'app/assets/javascripts/admin/statistics_panel/components/app.vue', + - 'app/assets/javascripts/badges/components/badge.vue', + - 'app/assets/javascripts/badges/components/badge_form.vue', + - 'app/assets/javascripts/batch_comments/components/draft_note.vue', + + 'app/assets/javascripts/batch_comments/components/preview_item.vue', + 'app/assets/javascripts/behaviors/components/json_table.vue', + 'app/assets/javascripts/behaviors/components/sandboxed_mermaid.vue', + DIFF + end + + before do + allow(instance).to receive(:filter_and_get_changed_files_in_mr).and_return(files) + allow(instance).to receive(:`) + .with('git diff HEAD~..HEAD -- .eslint_todo/vue-no-unused-properties.mjs') + .and_return(git_diff_output) + end + + it 'runs eslint with the correct arguments and returns exit 1 on failure' do + expect(instance).to receive(:system).with(*eslint_command).and_return(false) + + status = instance_double(Process::Status, exitstatus: 1) + allow(instance).to receive(:last_command_status).and_return(status) + + expect(instance.run_eslint_for_changed_files).to eq(1) + end + end + + context 'when several todo files have been changed' do + let(:eslint_command) do + ['yarn', 'run', 'lint:eslint', '--no-warn-ignored', '--format', 'gitlab', + '.eslint_todo/vue-no-unused-properties.mjs', + 'app/assets/javascripts/projects/project_new.js', + 'app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue', + 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue'] + end + + let(:files) { ['.eslint_todo/vue-no-unused-properties.mjs'] } + let(:git_diff_output) do + <<-DIFF + diff --git a/.eslint_todo/index.mjs b/.eslint_todo/index.mjs + index 88c73e337a50..1c27203d62cb 100644 + --- a/.eslint_todo/index.mjs + +++ b/.eslint_todo/index.mjs + @@ -1 +1,3 @@ + export { default as vueNoUnusedProperties } from './vue-no-unused-properties.mjs'; + + + +export { default as noUnusedVars } from './no-unused-vars.mjs'; + diff --git a/.eslint_todo/no-unused-vars.mjs b/.eslint_todo/no-unused-vars.mjs + index dbd4c28b65c8..f11106b14857 100644 + --- a/.eslint_todo/no-unused-vars.mjs + +++ b/.eslint_todo/no-unused-vars.mjs + @@ -3,7 +3,6 @@ + */ + export default { + files: [ + - 'app/assets/javascripts/projects/project_new.js', + 'app/assets/javascripts/vue_shared/components/customizable_dashboard/dashboard_editor/available_visualizations_drawer.vue', + 'app/assets/javascripts/vue_shared/components/customizable_dashboard/utils.js', + ], + diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs + index 81f0b2cbcf84..cf56bd55554b 100644 + --- a/.eslint_todo/vue-no-unused-properties.mjs + +++ b/.eslint_todo/vue-no-unused-properties.mjs + @@ -3,8 +3,6 @@ + */ + export default { + files: [ + - 'app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue', + - 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue', + 'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue', + 'app/assets/javascripts/admin/statistics_panel/components/app.vue', + 'app/assets/javascripts/badges/components/badge.vue', + + DIFF + end + + before do + allow(instance).to receive(:filter_and_get_changed_files_in_mr).and_return(files) + allow(instance).to receive(:`) + .with('git diff HEAD~..HEAD -- .eslint_todo/vue-no-unused-properties.mjs') + .and_return(git_diff_output) + end + + it 'runs eslint with the correct arguments and returns exit 1 on failure' do + expect(instance).to receive(:system).with(*eslint_command).and_return(false) + + status = instance_double(Process::Status, exitstatus: 1) + allow(instance).to receive(:last_command_status).and_return(status) + + expect(instance.run_eslint_for_changed_files).to eq(1) + end + end + + context 'when todo files have been changed but no ignored file was removed from them' do + let(:eslint_command) do + ['yarn', 'run', 'lint:eslint', '--no-warn-ignored', '--format', 'gitlab', + '.eslint_todo/vue-no-unused-properties.mjs'] + end + + let(:files) { ['.eslint_todo/vue-no-unused-properties.mjs'] } + let(:git_diff_output) do + <<-DIFF + diff --git a/.eslint_todo/no-unused-vars.mjs b/.eslint_todo/no-unused-vars.mjs + new file mode 100644 + index 000000000000..dbd4c28b65c8 + --- /dev/null + +++ b/.eslint_todo/no-unused-vars.mjs + @@ -0,0 +1,13 @@ + +/** + + * Generated by `scripts/frontend/generate_eslint_todo_list.mjs`. + + */ + +export default { + + files: [ + + 'app/assets/javascripts/projects/project_new.js', + + 'app/assets/javascripts/vue_shared/components/customizable_dashboard/dashboard_editor/available_visualizations_drawer.vue', + + 'app/assets/javascripts/vue_shared/components/customizable_dashboard/utils.js', + + ], + + rules: { + + 'no-unused-vars': 'off', + + }, + +}; + + DIFF + end + + before do + allow(instance).to receive(:filter_and_get_changed_files_in_mr).and_return(files) + allow(instance).to receive(:`) + .with('git diff HEAD~..HEAD -- .eslint_todo/vue-no-unused-properties.mjs') + .and_return(git_diff_output) + end + + it 'runs eslint with the correct arguments and returns exit 1 on failure' do + expect(instance).to receive(:system).with(*eslint_command).and_return(false) + + status = instance_double(Process::Status, exitstatus: 1) + allow(instance).to receive(:last_command_status).and_return(status) + + expect(instance.run_eslint_for_changed_files).to eq(1) + end + end end describe 'Run CLI commands' do diff --git a/spec/workers/namespaces/enable_descendants_cache_cron_worker_spec.rb b/spec/workers/namespaces/enable_descendants_cache_cron_worker_spec.rb index d299a93eea0..c6ce7b5812b 100644 --- a/spec/workers/namespaces/enable_descendants_cache_cron_worker_spec.rb +++ b/spec/workers/namespaces/enable_descendants_cache_cron_worker_spec.rb @@ -26,9 +26,13 @@ RSpec.describe Namespaces::EnableDescendantsCacheCronWorker, '#perform', :clean_ it 'creates the cache record for the top level group and the subgroup' do metadata = worker.perform - ids = Namespaces::Descendants.pluck(:namespace_id) + cache_records = Namespaces::Descendants.all + ids = cache_records.map(&:id) expect(ids).to match_array([group.id, subgroup.id]) + outdated_ats = cache_records.map(&:outdated_at) + expect(outdated_ats).to all(be_present) + expect(metadata).to eq({ over_time: false, last_id: nil, cache_count: 2 }) end diff --git a/tooling/ci/changed_files.rb b/tooling/ci/changed_files.rb index 920cd356552..4492af32b29 100755 --- a/tooling/ci/changed_files.rb +++ b/tooling/ci/changed_files.rb @@ -3,7 +3,10 @@ module CI class ChangedFiles - FRONTEND_FILES_FILTER = /\.(js|cjs|mjs|vue)$/ + FRONTEND_EXTENSIONS = "js|cjs|mjs|vue|graphql" + FRONTEND_FILES_FILTER = /\.(#{FRONTEND_EXTENSIONS})$/ + ESLINT_TODO_FILES = %r{\.eslint_todo/[a-z-]+\.mjs$} + ESLINT_TODO_REMOVED_FILE_PATHS = %r{- +'([a-z/_-]+\.(#{FRONTEND_EXTENSIONS}))'} def initialize(env: ENV, args: ARGV) @env = env @@ -18,6 +21,15 @@ module CI `git diff --name-only --diff-filter=d HEAD~..HEAD`.split("\n") end + def get_files_removed_from_todo_files(changed_files) + changed_todo_files = changed_files.filter { |file| file =~ ESLINT_TODO_FILES } + + return [] if changed_todo_files.empty? + + diff = `git diff HEAD~..HEAD -- #{changed_todo_files.join(' ')}` + diff.scan(ESLINT_TODO_REMOVED_FILE_PATHS).map(&:first) + end + def filter_and_get_changed_files_in_mr(filter_pattern: //) get_changed_files_in_merged_results_pipeline.grep(filter_pattern) end @@ -26,6 +38,7 @@ module CI puts 'Running ESLint for changed files...' files = filter_and_get_changed_files_in_mr(filter_pattern: FRONTEND_FILES_FILTER) + files += get_files_removed_from_todo_files(files) if files.empty? puts 'No files were changed. Skipping...'