diff options
119 files changed, 1169 insertions, 1159 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 14bc169854a..37c5c2ed238 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -241,10 +241,6 @@ Style/KeywordParametersOrder: Style/Lambda: Enabled: false -# Offense count: 21 -Style/MissingRespondToMissing: - Enabled: false - # Offense count: 35 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinBodyLength. diff --git a/.rubocop_todo/database/multiple_databases.yml b/.rubocop_todo/database/multiple_databases.yml index 019fb41094d..40de71b1203 100644 --- a/.rubocop_todo/database/multiple_databases.yml +++ b/.rubocop_todo/database/multiple_databases.yml @@ -14,34 +14,9 @@ Database/MultipleDatabases: - 'ee/spec/services/ee/merge_requests/update_service_spec.rb' - 'lib/backup/database.rb' - 'lib/backup/manager.rb' - - lib/gitlab/background_migration/backfill_integrations_type_new.rb - - lib/gitlab/background_migration/backfill_issue_search_data.rb - - lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb - - lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb - - lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb - - lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb - lib/gitlab/background_migration/backfill_projects_with_coverage.rb - - lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb - - lib/gitlab/background_migration/backfill_user_namespace.rb - lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans.rb - - lib/gitlab/background_migration/delete_orphaned_deployments.rb - - lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb - - lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb - - lib/gitlab/background_migration/fix_projects_without_project_feature.rb - - lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb - - lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb - - lib/gitlab/background_migration/migrate_stage_status.rb - - lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb - lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb - - lib/gitlab/background_migration/populate_container_repository_migration_plan.rb - - lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb - - lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb - - lib/gitlab/background_migration/populate_vulnerability_reads.rb - - lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb - - lib/gitlab/background_migration/remove_vulnerability_finding_links.rb - - lib/gitlab/background_migration/update_timelogs_null_spent_at.rb - - lib/gitlab/background_migration/update_timelogs_project_id.rb - - lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb - 'lib/gitlab/database.rb' - 'lib/gitlab/database/load_balancing/load_balancer.rb' - 'lib/gitlab/database/migrations/observers/query_log.rb' diff --git a/.rubocop_todo/migration/background_migration_record.yml b/.rubocop_todo/migration/background_migration_record.yml index 7699d508da5..6ae518a86fb 100644 --- a/.rubocop_todo/migration/background_migration_record.yml +++ b/.rubocop_todo/migration/background_migration_record.yml @@ -28,7 +28,6 @@ Migration/BackgroundMigrationRecord: - lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb - lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb - lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics.rb - - lib/gitlab/background_migration/migrate_stage_status.rb - lib/gitlab/background_migration/migrate_u2f_webauthn.rb - lib/gitlab/background_migration/populate_latest_pipeline_ids.rb - lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb @@ -54,4 +53,4 @@ Migration/BackgroundMigrationRecord: - ee/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column.rb - ee/lib/ee/gitlab/background_migration/populate_status_column_of_security_scans.rb - ee/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings.rb - - ee/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location.rb
\ No newline at end of file + - ee/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location.rb diff --git a/.rubocop_todo/style/missing_respond_to_missing.yml b/.rubocop_todo/style/missing_respond_to_missing.yml new file mode 100644 index 00000000000..4d602586342 --- /dev/null +++ b/.rubocop_todo/style/missing_respond_to_missing.yml @@ -0,0 +1,27 @@ +--- +Style/MissingRespondToMissing: + # Offense count: 21 + # Temporarily disabled due to too many offenses + Enabled: false + Exclude: + - 'app/controllers/projects/application_controller.rb' + - 'app/models/network/commit.rb' + - 'app/services/notification_service.rb' + - 'ee/app/controllers/ee/groups/application_controller.rb' + - 'ee/app/models/elastic/migration_record.rb' + - 'ee/app/services/ee/audit_event_service.rb' + - 'lib/declarative_enum.rb' + - 'lib/gitlab/auth/ldap/dn.rb' + - 'lib/gitlab/fake_application_settings.rb' + - 'lib/gitlab/gitaly_client/storage_settings.rb' + - 'lib/gitlab/graphql/batch_key.rb' + - 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb' + - 'lib/gitlab/legacy_github_import/client.rb' + - 'lib/gitlab/metrics/null_metric.rb' + - 'lib/gitlab/tracking/event_definition.rb' + - 'lib/kramdown/parser/atlassian_document_format.rb' + - 'lib/mattermost/session.rb' + - 'lib/uploaded_file.rb' + - 'qa/qa/runtime/release.rb' + - 'qa/qa/runtime/scenario.rb' + - 'spec/support/helpers/next_found_instance_of.rb' diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index 866148596e3..d0c4ad3646c 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -57,6 +57,9 @@ export default { update({ currentLicense }) { return currentLicense?.plan; }, + error() { + this.hasCurrentLicenseFetchError = true; + }, }, }, props: { @@ -99,6 +102,7 @@ export default { autoDevopsEnabledAlertDismissedProjects: [], errorMessage: '', currentLicensePlan: '', + hasCurrentLicenseFetchError: false, }; }, computed: { @@ -120,7 +124,10 @@ export default { ); }, shouldShowVulnerabilityManagementTab() { - return this.currentLicensePlan === LICENSE_ULTIMATE; + // if the query fails (if the plan is `null` also means an error has occurred) we still want to show the feature + const hasQueryError = this.hasCurrentLicenseFetchError || this.currentLicensePlan === null; + + return hasQueryError || this.currentLicensePlan === LICENSE_ULTIMATE; }, }, methods: { diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 54df021e4f8..97b3c4c6a2f 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -173,16 +173,12 @@ class Clusters::ClustersController < Clusters::BaseController private - def certificate_based_clusters_enabled? - Feature.enabled?(:certificate_based_clusters, clusterable.clusterable_namespace, default_enabled: :yaml, type: :ops) - end - def ensure_feature_enabled! - render_404 unless certificate_based_clusters_enabled? + render_404 unless clusterable.certificate_based_clusters_enabled? end def cluster_list - return [] unless certificate_based_clusters_enabled? + return [] unless clusterable.certificate_based_clusters_enabled? finder = ClusterAncestorsFinder.new(clusterable.__subject__, current_user) clusters = finder.execute diff --git a/app/graphql/mutations/environments/canary_ingress/update.rb b/app/graphql/mutations/environments/canary_ingress/update.rb index ce24b8842c6..b5633f49809 100644 --- a/app/graphql/mutations/environments/canary_ingress/update.rb +++ b/app/graphql/mutations/environments/canary_ingress/update.rb @@ -24,7 +24,7 @@ module Mutations 'https://gitlab.com/groups/gitlab-org/configure/-/epics/8' def resolve(id:, **kwargs) - return { errors: [REMOVAL_ERR_MSG] } if cert_based_clusters_ff_disabled? + return { errors: [REMOVAL_ERR_MSG] } unless certificate_based_clusters_enabled? environment = authorized_find!(id: id) @@ -43,8 +43,9 @@ module Mutations private - def cert_based_clusters_ff_disabled? - Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops) + def certificate_based_clusters_enabled? + instance_cluster = ::Clusters::Instance.new + instance_cluster.certificate_based_clusters_enabled? end end end diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb index a62ef6d76e5..50e3b64448c 100644 --- a/app/graphql/resolvers/design_management/designs_resolver.rb +++ b/app/graphql/resolvers/design_management/designs_resolver.rb @@ -24,6 +24,10 @@ module Resolvers end def resolve(ids: nil, filenames: nil, at_version: nil) + # TODO: remove the coercion when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + context.scoped_set!(:at_version_argument, VersionID.coerce_isolated_input(at_version)) if at_version + ::DesignManagement::DesignsFinder.new( issue, current_user, diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb index 23ba3c86d98..df283104821 100644 --- a/app/graphql/resolvers/design_management/versions_resolver.rb +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -9,8 +9,6 @@ module Resolvers VersionID = ::Types::GlobalIDType[::DesignManagement::Version] - extras [:parent] - argument :earlier_or_equal_to_sha, GraphQL::Types::String, as: :sha, required: false, @@ -26,11 +24,11 @@ module Resolvers ::Resolvers::DesignManagement::VersionInCollectionResolver end - def resolve(parent: nil, id: nil, sha: nil) + def resolve(id: nil, sha: nil) # TODO: remove this line when the compatibility layer is removed # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 id &&= VersionID.coerce_isolated_input(id) - version = cutoff(parent, id, sha) + version = cutoff(id, sha) raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present? @@ -44,11 +42,11 @@ module Resolvers private # Find the most recent version that the client will accept - def cutoff(parent, id, sha) + def cutoff(id, sha) if sha.present? || id.present? specific_version(id, sha) - elsif at_version = at_version_arg(parent) - by_id(at_version) + elsif at_version = context[:at_version_argument] + by_id(at_version) # See: DesignsResolver else :unconstrained end @@ -68,20 +66,6 @@ module Resolvers def by_id(gid) ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(gid)) end - - # Find an `at_version` argument passed to a parent node. - # - # If one is found, then a design collection further up the AST - # has been filtered to reflect designs at that version, and so - # for consistency we should only present versions up to the given - # version here. - def at_version_arg(parent) - # TODO: remove coercion when the compatibility layer is removed - # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 - version_id = ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4) - version_id &&= VersionID.coerce_isolated_input(version_id) - version_id - end end end end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb index 4c0b1162306..cc4c0e19ec7 100644 --- a/app/graphql/types/design_management/design_type.rb +++ b/app/graphql/types/design_management/design_type.rb @@ -23,14 +23,13 @@ module Types field :versions, Types::DesignManagement::VersionType.connection_type, resolver: Resolvers::DesignManagement::VersionsResolver, - description: "All versions related to this design ordered newest first.", - extras: [:parent] + description: "All versions related to this design ordered newest first." # Returns a `DesignManagement::Version` for this query based on the # `atVersion` argument passed to a parent node if present, or otherwise # the most recent `Version` for the issue. def cached_stateful_version(parent_node) - version_gid = Gitlab::Graphql::FindArgumentInParent.find(parent_node, :at_version) + version_gid = context[:at_version_argument] # See: DesignsResolver # Caching is scoped to an `issue_id` to allow us to cache the # most recent `Version` for an issue diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index dbdfa0c1eab..aa0e8c55470 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -462,7 +462,7 @@ module ApplicationSettingsHelper def instance_clusters_enabled? clusterable = Clusters::Instance.new - Feature.enabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops) && + clusterable.certificate_based_clusters_enabled? && can?(current_user, :read_cluster, clusterable) end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 116ff284c16..6a721f600f5 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -23,7 +23,7 @@ module ClustersHelper can_add_cluster: clusterable.can_add_cluster?.to_s, can_admin_cluster: clusterable.can_admin_cluster?.to_s, display_cluster_agents: display_cluster_agents?(clusterable).to_s, - certificate_based_clusters_enabled: Feature.enabled?(:certificate_based_clusters, clusterable.clusterable_namespace, default_enabled: :yaml, type: :ops).to_s, + certificate_based_clusters_enabled: clusterable.certificate_based_clusters_enabled?.to_s, default_branch_name: default_branch_name(clusterable), project_path: clusterable_project_path(clusterable), kas_address: Gitlab::Kas.external_url, diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index dff8bb89021..0f4cac7f5b4 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -45,7 +45,7 @@ module Ci dotenv: '.env', cobertura: 'cobertura-coverage.xml', terraform: 'tfplan.json', - cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/333441 + cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094 requirements: 'requirements.json', coverage_fuzzing: 'gl-coverage-fuzzing.json', api_fuzzing: 'gl-api-fuzzing-report.json' @@ -64,7 +64,7 @@ module Ci network_referee: :gzip, dotenv: :gzip, cobertura: :gzip, - cluster_applications: :gzip, + cluster_applications: :gzip, # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094 lsif: :zip, # Security reports and license scanning reports are raw artifacts diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb index 2a09ba11564..ef120384adf 100644 --- a/app/models/clusters/instance.rb +++ b/app/models/clusters/instance.rb @@ -9,5 +9,11 @@ module Clusters def flipper_id self.class.to_s end + + def certificate_based_clusters_enabled? + ::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:") do + Feature.enabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops) + end + end end end diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb index 9ee28916553..2b5e1a204cb 100644 --- a/app/models/concerns/deployment_platform.rb +++ b/app/models/concerns/deployment_platform.rb @@ -3,7 +3,7 @@ module DeploymentPlatform # rubocop:disable Gitlab/ModuleWithInstanceVariables def deployment_platform(environment: nil) - return if Feature.disabled?(:certificate_based_clusters, self.namespace, default_enabled: :yaml, type: :ops) + return unless self.namespace.certificate_based_clusters_enabled? @deployment_platform ||= {} diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 63d531d82c3..4204ad707b2 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -70,6 +70,11 @@ class Deployment < ApplicationRecord transition created: :blocked end + # This transition is possible when we have manual jobs. + event :create do + transition skipped: :created + end + event :unblock do transition blocked: :created end @@ -348,7 +353,7 @@ class Deployment < ApplicationRecord def sync_status_with(build) return false unless ::Deployment.statuses.include?(build.status) - return false if build.created? || build.status == self.status + return false if build.status == self.status update_status!(build.status) rescue StandardError => e @@ -403,6 +408,8 @@ class Deployment < ApplicationRecord skip! when 'blocked' block! + when 'created' + create! else raise ArgumentError, "The status #{status.inspect} is invalid" end diff --git a/app/models/integration.rb b/app/models/integration.rb index 13ef37e0157..c905788ac8b 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -161,7 +161,7 @@ class Integration < ApplicationRecord end def fields - self.class.fields + self.class.fields.dup end # Provide convenient accessor methods for each serialized property. diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index b384a94d713..4e144a688f6 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -5,7 +5,26 @@ module Integrations include ReactivelyCached prepend EnableSslVerification - prop_accessor :bamboo_url, :build_key, :username, :password + field :bamboo_url, + title: s_('BambooService|Bamboo URL'), + placeholder: s_('https://bamboo.example.com'), + help: s_('BambooService|Bamboo service root URL.'), + required: true + + field :build_key, + help: s_('BambooService|Bamboo build plan key.'), + non_empty_password_title: s_('BambooService|Enter new build key'), + non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'), + placeholder: s_('KEY'), + required: true + + field :username, + help: s_('BambooService|The user with API access to the Bamboo server.') + + field :password, + type: 'password', + non_empty_password_title: s_('ProjectService|Enter new password'), + non_empty_password_help: s_('ProjectService|Leave blank to use your current password') validates :bamboo_url, presence: true, public_url: true, if: :activated? validates :build_key, presence: true, if: :activated? @@ -43,39 +62,6 @@ module Integrations 'bamboo' end - def fields - [ - { - type: 'text', - name: 'bamboo_url', - title: s_('BambooService|Bamboo URL'), - placeholder: s_('https://bamboo.example.com'), - help: s_('BambooService|Bamboo service root URL.'), - required: true - }, - { - type: 'password', - name: 'build_key', - help: s_('BambooService|Bamboo build plan key.'), - non_empty_password_title: s_('BambooService|Enter new build key'), - non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'), - placeholder: s_('KEY'), - required: true - }, - { - type: 'text', - name: 'username', - help: s_('BambooService|The user with API access to the Bamboo server.') - }, - { - type: 'password', - name: 'password', - non_empty_password_title: s_('ProjectService|Enter new password'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current password') - } - ] - end - def build_page(sha, ref) with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index 3b802271a36..d1e54ce86ee 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -10,7 +10,18 @@ module Integrations ENDPOINT = "https://buildkite.com" - prop_accessor :project_url, :token + field :project_url, + title: _('Pipeline URL'), + placeholder: "#{ENDPOINT}/example-org/test-pipeline", + required: true + + field :token, + type: 'password', + title: _('Token'), + help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'), + non_empty_password_title: s_('ProjectService|Enter new token'), + non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'), + required: true validates :project_url, presence: true, public_url: true, if: :activated? validates :token, presence: true, if: :activated? @@ -74,24 +85,6 @@ module Integrations s_('ProjectService|Run CI/CD pipelines with Buildkite.') end - def fields - [ - { type: 'password', - name: 'token', - title: _('Token'), - help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'), - non_empty_password_title: s_('ProjectService|Enter new token'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'), - required: true }, - - { type: 'text', - name: 'project_url', - title: _('Pipeline URL'), - placeholder: "#{ENDPOINT}/example-org/test-pipeline", - required: true } - ] - end - def calculate_reactive_cache(sha, ref) response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options) diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index 73f78bec381..0c65ed8cd5f 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -10,7 +10,17 @@ module Integrations DRONE_SAAS_HOSTNAME = 'cloud.drone.io' - prop_accessor :drone_url, :token + field :drone_url, + title: s_('ProjectService|Drone server URL'), + placeholder: 'http://drone.example.com', + required: true + + field :token, + type: 'password', + help: s_('ProjectService|Token for the Drone project.'), + non_empty_password_title: s_('ProjectService|Enter new token'), + non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'), + required: true validates :drone_url, presence: true, public_url: true, if: :activated? validates :token, presence: true, if: :activated? @@ -94,26 +104,6 @@ module Integrations s_('ProjectService|Run CI/CD pipelines with Drone.') end - def fields - [ - { - type: 'password', - name: 'token', - help: s_('ProjectService|Token for the Drone project.'), - non_empty_password_title: s_('ProjectService|Enter new token'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'), - required: true - }, - { - type: 'text', - name: 'drone_url', - title: s_('ProjectService|Drone server URL'), - placeholder: 'http://drone.example.com', - required: true - } - ] - end - override :hook_url def hook_url [drone_url, "/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index 83838ac1b53..a1abbce72bc 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -7,7 +7,25 @@ module Integrations prepend EnableSslVerification extend Gitlab::Utils::Override - prop_accessor :jenkins_url, :project_name, :username, :password + field :jenkins_url, + title: s_('ProjectService|Jenkins server URL'), + required: true, + placeholder: 'http://jenkins.example.com', + help: s_('The URL of the Jenkins server.') + + field :project_name, + required: true, + placeholder: 'my_project_name', + help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.') + + field :username, + help: s_('The username for the Jenkins server.') + + field :password, + type: 'password', + help: s_('The password for the Jenkins server.'), + non_empty_password_title: s_('ProjectService|Enter new password.'), + non_empty_password_help: s_('ProjectService|Leave blank to use your current password.') before_validation :reset_password @@ -71,37 +89,5 @@ module Integrations def self.to_param 'jenkins' end - - def fields - [ - { - type: 'text', - name: 'jenkins_url', - title: s_('ProjectService|Jenkins server URL'), - required: true, - placeholder: 'http://jenkins.example.com', - help: s_('The URL of the Jenkins server.') - }, - { - type: 'text', - name: 'project_name', - required: true, - placeholder: 'my_project_name', - help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.') - }, - { - type: 'text', - name: 'username', - help: s_('The username for the Jenkins server.') - }, - { - type: 'password', - name: 'password', - help: s_('The password for the Jenkins server.'), - non_empty_password_title: s_('ProjectService|Enter new password.'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current password.') - } - ] - end end end diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb index 568fb609a44..cd2928136ef 100644 --- a/app/models/integrations/mock_ci.rb +++ b/app/models/integrations/mock_ci.rb @@ -7,7 +7,11 @@ module Integrations ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze - prop_accessor :mock_service_url + field :mock_service_url, + title: s_('ProjectService|Mock service URL'), + placeholder: 'http://localhost:4004', + required: true + validates :mock_service_url, presence: true, public_url: true, if: :activated? def title @@ -22,18 +26,6 @@ module Integrations 'mock_ci' end - def fields - [ - { - type: 'text', - name: 'mock_service_url', - title: s_('ProjectService|Mock service URL'), - placeholder: 'http://localhost:4004', - required: true - } - ] - end - # Return complete url to build page # # Ex. diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index f0f83f118d7..1205173e40b 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -8,7 +8,22 @@ module Integrations TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze - prop_accessor :teamcity_url, :build_type, :username, :password + field :teamcity_url, + title: s_('ProjectService|TeamCity server URL'), + placeholder: 'https://teamcity.example.com', + required: true + + field :build_type, + help: s_('ProjectService|The build configuration ID of the TeamCity project.'), + required: true + + field :username, + help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.') + + field :password, + type: 'password', + non_empty_password_title: s_('ProjectService|Enter new password'), + non_empty_password_help: s_('ProjectService|Leave blank to use your current password') validates :teamcity_url, presence: true, public_url: true, if: :activated? validates :build_type, presence: true, if: :activated? @@ -51,35 +66,6 @@ module Integrations s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.') end - def fields - [ - { - type: 'text', - name: 'teamcity_url', - title: s_('ProjectService|TeamCity server URL'), - placeholder: 'https://teamcity.example.com', - required: true - }, - { - type: 'text', - name: 'build_type', - help: s_('ProjectService|The build configuration ID of the TeamCity project.'), - required: true - }, - { - type: 'text', - name: 'username', - help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.') - }, - { - type: 'password', - name: 'password', - non_empty_password_title: s_('ProjectService|Enter new password'), - non_empty_password_help: s_('ProjectService|Leave blank to use your current password') - } - ] - end - def build_page(sha, ref) with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index fa9e8152fa9..32f44c6cfcc 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -525,6 +525,12 @@ class Namespace < ApplicationRecord nil end + def certificate_based_clusters_enabled? + ::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:ns:#{self.id}") do + Feature.enabled?(:certificate_based_clusters, self, default_enabled: :yaml, type: :ops) + end + end + private def expire_child_caches diff --git a/app/models/project.rb b/app/models/project.rb index eeafb188fc4..f1e2967fb0e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -465,7 +465,7 @@ class Project < ApplicationRecord delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role, to: :team delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true - delegate :root_ancestor, to: :namespace, allow_nil: true + delegate :root_ancestor, :certificate_based_clusters_enabled?, to: :namespace, allow_nil: true delegate :last_pipeline, to: :commit, allow_nil: true delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb index f8ad3e18505..c2ed40d8b0c 100644 --- a/app/presenters/clusterable_presenter.rb +++ b/app/presenters/clusterable_presenter.rb @@ -12,11 +12,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated .fabricate! end - def clusterable_namespace - return clusterable.namespace if clusterable.is_a?(Project) - return clusterable if clusterable.is_a?(Group) - end - def can_add_cluster? can?(current_user, :add_cluster, clusterable) end diff --git a/config/feature_flags/development/route_hll_to_snowplow.yml b/config/feature_flags/development/route_hll_to_snowplow.yml index 3c8f7826a0a..0caaf1446ca 100644 --- a/config/feature_flags/development/route_hll_to_snowplow.yml +++ b/config/feature_flags/development/route_hll_to_snowplow.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354442 milestone: '14.9' type: development group: group::product intelligence -default_enabled: false +default_enabled: true diff --git a/db/docs/alert_management_alert_assignees.yml b/db/docs/alert_management_alert_assignees.yml index 6b91d6db72c..4d5a1acb0ca 100644 --- a/db/docs/alert_management_alert_assignees.yml +++ b/db/docs/alert_management_alert_assignees.yml @@ -4,6 +4,6 @@ classes: - AlertManagement::AlertAssignee feature_categories: - incident_management -description: TODO +description: Persists metadata between users and alerts to support alert assignments introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32609 milestone: '13.1' diff --git a/db/docs/alert_management_alert_metric_images.yml b/db/docs/alert_management_alert_metric_images.yml index e43be4f854c..bcfa2b54b1c 100644 --- a/db/docs/alert_management_alert_metric_images.yml +++ b/db/docs/alert_management_alert_metric_images.yml @@ -4,6 +4,6 @@ classes: - AlertManagement::MetricImage feature_categories: - incident_management -description: TODO +description: Persists metadata for uploads related to alerts introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80339 milestone: '14.8' diff --git a/db/docs/alert_management_alert_user_mentions.yml b/db/docs/alert_management_alert_user_mentions.yml index eda29a17cda..6a3aaf2ce83 100644 --- a/db/docs/alert_management_alert_user_mentions.yml +++ b/db/docs/alert_management_alert_user_mentions.yml @@ -4,6 +4,6 @@ classes: - AlertManagement::AlertUserMention feature_categories: - incident_management -description: TODO +description: Persists metadata for system notes related to alerts introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33217 milestone: '13.1' diff --git a/db/docs/alert_management_alerts.yml b/db/docs/alert_management_alerts.yml index 347653fbb0b..ca8b02ec346 100644 --- a/db/docs/alert_management_alerts.yml +++ b/db/docs/alert_management_alerts.yml @@ -4,6 +4,6 @@ classes: - AlertManagement::Alert feature_categories: - incident_management -description: TODO +description: Persists incoming alert data including its payload introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29864 milestone: '13.0' diff --git a/db/docs/alert_management_http_integrations.yml b/db/docs/alert_management_http_integrations.yml index 7dba70bcb67..8fa330f0775 100644 --- a/db/docs/alert_management_http_integrations.yml +++ b/db/docs/alert_management_http_integrations.yml @@ -4,6 +4,6 @@ classes: - AlertManagement::HttpIntegration feature_categories: - incident_management -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44098 +description: Persists settings for alert HTTP integrations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43634 milestone: '13.5' diff --git a/db/docs/clusters.yml b/db/docs/clusters.yml index 142186e4410..cad0fd4c3b3 100644 --- a/db/docs/clusters.yml +++ b/db/docs/clusters.yml @@ -3,7 +3,7 @@ table_name: clusters classes: - Clusters::Cluster feature_categories: -- incident_management -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/d0cff7f5855f91b5479f9fdaa39d8d95ec691a9e +- kubernetes_management +description: Persists information about GitLab managed clusters +introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14879 milestone: '10.1' diff --git a/db/docs/clusters_integration_prometheus.yml b/db/docs/clusters_integration_prometheus.yml index 78a1933a931..f8702226daa 100644 --- a/db/docs/clusters_integration_prometheus.yml +++ b/db/docs/clusters_integration_prometheus.yml @@ -4,6 +4,6 @@ classes: - Clusters::Integrations::Prometheus feature_categories: - incident_management -description: TODO +description: Persists information about prometheus cluster integration introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59091 milestone: '13.11' diff --git a/db/docs/error_tracking_client_keys.yml b/db/docs/error_tracking_client_keys.yml index b9c7b74f0a3..c07fc282839 100644 --- a/db/docs/error_tracking_client_keys.yml +++ b/db/docs/error_tracking_client_keys.yml @@ -4,6 +4,6 @@ classes: - ErrorTracking::ClientKey feature_categories: - error_tracking -description: TODO +description: Model to store public keys used by Sentry SDK for Error Tracking introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66466 milestone: '14.2' diff --git a/db/docs/error_tracking_error_events.yml b/db/docs/error_tracking_error_events.yml index 19a7f1cbfe8..9d938e47e3c 100644 --- a/db/docs/error_tracking_error_events.yml +++ b/db/docs/error_tracking_error_events.yml @@ -4,6 +4,6 @@ classes: - ErrorTracking::ErrorEvent feature_categories: - error_tracking -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/2d1c197ab0bf10071cb52e579edd3808cb0adc21 +description: Persists error event data for the Error Tracking's GitLab backend +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64712 milestone: '14.1' diff --git a/db/docs/error_tracking_errors.yml b/db/docs/error_tracking_errors.yml index b348d47c77f..a961d759da3 100644 --- a/db/docs/error_tracking_errors.yml +++ b/db/docs/error_tracking_errors.yml @@ -4,6 +4,6 @@ classes: - ErrorTracking::Error feature_categories: - error_tracking -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/2d1c197ab0bf10071cb52e579edd3808cb0adc21 +description: Persists error data for the Error Tracking's GitLab backend +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64712 milestone: '14.1' diff --git a/db/docs/incident_management_escalation_policies.yml b/db/docs/incident_management_escalation_policies.yml index f8bb51957fd..9584f65b14d 100644 --- a/db/docs/incident_management_escalation_policies.yml +++ b/db/docs/incident_management_escalation_policies.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::EscalationPolicy feature_categories: - incident_management -description: TODO +description: Persists information about escalation policies in a project introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60685 milestone: '13.12' diff --git a/db/docs/incident_management_escalation_rules.yml b/db/docs/incident_management_escalation_rules.yml index 6f97d8b6524..40c1f9bdcc0 100644 --- a/db/docs/incident_management_escalation_rules.yml +++ b/db/docs/incident_management_escalation_rules.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::EscalationRule feature_categories: - incident_management -description: TODO +description: Persists information about escalation rules for incident management introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60685 milestone: '13.12' diff --git a/db/docs/incident_management_issuable_escalation_statuses.yml b/db/docs/incident_management_issuable_escalation_statuses.yml index 4c0010f212a..466bc0314b6 100644 --- a/db/docs/incident_management_issuable_escalation_statuses.yml +++ b/db/docs/incident_management_issuable_escalation_statuses.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::IssuableEscalationStatus feature_categories: - incident_management -description: TODO +description: Persists escalation status information for incidents introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65206 milestone: '14.2' diff --git a/db/docs/incident_management_oncall_participants.yml b/db/docs/incident_management_oncall_participants.yml index a394afae79b..9186be4824c 100644 --- a/db/docs/incident_management_oncall_participants.yml +++ b/db/docs/incident_management_oncall_participants.yml @@ -4,6 +4,7 @@ classes: - IncidentManagement::OncallParticipant feature_categories: - incident_management -description: TODO +- on_call_schedule_management +description: Persists information about on-call rotation participants introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49058 milestone: '13.7' diff --git a/db/docs/incident_management_oncall_rotations.yml b/db/docs/incident_management_oncall_rotations.yml index 114973d12be..bf9df2e2d2a 100644 --- a/db/docs/incident_management_oncall_rotations.yml +++ b/db/docs/incident_management_oncall_rotations.yml @@ -4,6 +4,7 @@ classes: - IncidentManagement::OncallRotation feature_categories: - incident_management -description: TODO +- on_call_schedule_management +description: Persists information about on-call rotation introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49058 milestone: '13.7' diff --git a/db/docs/incident_management_oncall_schedules.yml b/db/docs/incident_management_oncall_schedules.yml index deb05826e08..9fa0ed1bec5 100644 --- a/db/docs/incident_management_oncall_schedules.yml +++ b/db/docs/incident_management_oncall_schedules.yml @@ -4,6 +4,7 @@ classes: - IncidentManagement::OncallSchedule feature_categories: - incident_management -description: TODO +- on_call_schedule_management +description: Persists on-call schedules for incident management in a project introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47407 milestone: '13.7' diff --git a/db/docs/incident_management_oncall_shifts.yml b/db/docs/incident_management_oncall_shifts.yml index 2e1a12032d4..6ef7de5da50 100644 --- a/db/docs/incident_management_oncall_shifts.yml +++ b/db/docs/incident_management_oncall_shifts.yml @@ -4,6 +4,7 @@ classes: - IncidentManagement::OncallShift feature_categories: - incident_management -description: TODO +- on_call_schedule_management +description: Tracks past and present on-call shifts introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49423 milestone: '13.8' diff --git a/db/docs/incident_management_pending_alert_escalations.yml b/db/docs/incident_management_pending_alert_escalations.yml index 5c8f456f084..c39b8d74ce2 100644 --- a/db/docs/incident_management_pending_alert_escalations.yml +++ b/db/docs/incident_management_pending_alert_escalations.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::PendingEscalations::Alert feature_categories: - incident_management -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e5cc867503dfbd54f99df90cce6be39bc4fde712 +description: Persists information about pending alert escalations for incidents +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64274 milestone: '14.1' diff --git a/db/docs/incident_management_pending_issue_escalations.yml b/db/docs/incident_management_pending_issue_escalations.yml index 0956b1f9453..eb8f11fc72f 100644 --- a/db/docs/incident_management_pending_issue_escalations.yml +++ b/db/docs/incident_management_pending_issue_escalations.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::PendingEscalations::Issue feature_categories: - incident_management -description: TODO +description: Represents when issues should be escalated according to a project's escalation policy introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65209 milestone: '14.3' diff --git a/db/docs/incident_management_timeline_events.yml b/db/docs/incident_management_timeline_events.yml index d13a6ec29ec..6031f0d32e2 100644 --- a/db/docs/incident_management_timeline_events.yml +++ b/db/docs/incident_management_timeline_events.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::TimelineEvent feature_categories: - incident_management -description: TODO +description: Persists timeline events for an incident introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74530 milestone: '14.6' diff --git a/db/docs/issuable_slas.yml b/db/docs/issuable_slas.yml index 631086f68a9..14e94a3aed3 100644 --- a/db/docs/issuable_slas.yml +++ b/db/docs/issuable_slas.yml @@ -4,6 +4,6 @@ classes: - IssuableSla feature_categories: - incident_management -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44928 +description: Persists information about incident SLAs for incidents +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44253 milestone: '13.5' diff --git a/db/docs/issues_prometheus_alert_events.yml b/db/docs/issues_prometheus_alert_events.yml index 4a222ab3924..01ff7f9b6e4 100644 --- a/db/docs/issues_prometheus_alert_events.yml +++ b/db/docs/issues_prometheus_alert_events.yml @@ -3,6 +3,6 @@ table_name: issues_prometheus_alert_events classes: [] feature_categories: - incident_management -description: TODO +description: Adds relationship between PrometheusAlertEvent and issues created due to them introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17477 milestone: '12.4' diff --git a/db/docs/issues_self_managed_prometheus_alert_events.yml b/db/docs/issues_self_managed_prometheus_alert_events.yml index 016e1f17a6e..feb208d7c3a 100644 --- a/db/docs/issues_self_managed_prometheus_alert_events.yml +++ b/db/docs/issues_self_managed_prometheus_alert_events.yml @@ -3,6 +3,6 @@ table_name: issues_self_managed_prometheus_alert_events classes: [] feature_categories: - incident_management -description: TODO +description: Adds associations between Issues table and Prometheus alerts from self-managed Prometheus instances introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18046 milestone: '12.4' diff --git a/db/docs/project_alerting_settings.yml b/db/docs/project_alerting_settings.yml index 60a23e35f71..0737c65faaf 100644 --- a/db/docs/project_alerting_settings.yml +++ b/db/docs/project_alerting_settings.yml @@ -4,6 +4,6 @@ classes: - Alerting::ProjectAlertingSetting feature_categories: - incident_management -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/39aa9458c282c1dabd3623698da5af3b9a6122a9 +description: Persists project-level tokens for manual Prometheus installations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9334 milestone: '11.8' diff --git a/db/docs/project_error_tracking_settings.yml b/db/docs/project_error_tracking_settings.yml index 0a1e2571743..d10982fe712 100644 --- a/db/docs/project_error_tracking_settings.yml +++ b/db/docs/project_error_tracking_settings.yml @@ -4,6 +4,6 @@ classes: - ErrorTracking::ProjectErrorTrackingSetting feature_categories: - error_tracking -description: TODO -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/f40b5860d76a8ea5d964260834a6e83516b0f1fd +description: Project settings related to Error Tracking +introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24047 milestone: '11.7' diff --git a/db/docs/project_incident_management_settings.yml b/db/docs/project_incident_management_settings.yml index 55413824f40..b1ef6824fe2 100644 --- a/db/docs/project_incident_management_settings.yml +++ b/db/docs/project_incident_management_settings.yml @@ -4,6 +4,6 @@ classes: - IncidentManagement::ProjectIncidentManagementSetting feature_categories: - incident_management -description: TODO +description: Persists project settings for incident management introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9744 milestone: '11.9' diff --git a/db/docs/prometheus_alerts.yml b/db/docs/prometheus_alerts.yml index 0e017292f81..3d3a2e45650 100644 --- a/db/docs/prometheus_alerts.yml +++ b/db/docs/prometheus_alerts.yml @@ -4,6 +4,6 @@ classes: - PrometheusAlert feature_categories: - incident_management -description: TODO +description: Persists information about prometheus alerts from an environment introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590 milestone: '11.2' diff --git a/db/docs/sentry_issues.yml b/db/docs/sentry_issues.yml index 898d4ddbae1..af96751fc7d 100644 --- a/db/docs/sentry_issues.yml +++ b/db/docs/sentry_issues.yml @@ -4,6 +4,6 @@ classes: - SentryIssue feature_categories: - error_tracking -description: TODO +description: Persists issue data for the Error Tracking's Sentry backend introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20629 milestone: '12.6' diff --git a/db/docs/status_page_published_incidents.yml b/db/docs/status_page_published_incidents.yml index 562e8eb2f80..4a21ed156f2 100644 --- a/db/docs/status_page_published_incidents.yml +++ b/db/docs/status_page_published_incidents.yml @@ -4,6 +4,6 @@ classes: - StatusPage::PublishedIncident feature_categories: - incident_management -description: TODO +description: Corresponds to an issue which has been published to the Status Page introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29994 milestone: '13.0' diff --git a/db/docs/status_page_settings.yml b/db/docs/status_page_settings.yml index b4022bdb678..a5cefe70300 100644 --- a/db/docs/status_page_settings.yml +++ b/db/docs/status_page_settings.yml @@ -4,6 +4,6 @@ classes: - StatusPage::ProjectSetting feature_categories: - incident_management -description: TODO +description: Project settings related to Status Page introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25863 milestone: '12.9' diff --git a/db/docs/zoom_meetings.yml b/db/docs/zoom_meetings.yml index 0c1a47eb6d0..620df953ad5 100644 --- a/db/docs/zoom_meetings.yml +++ b/db/docs/zoom_meetings.yml @@ -4,6 +4,6 @@ classes: - ZoomMeeting feature_categories: - incident_management -description: TODO +description: Persists Zoom meetings, its associations and its metadata introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17890 milestone: '12.5' diff --git a/doc/development/changelog.md b/doc/development/changelog.md index b98ed6cb109..c19c5b40382 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -100,7 +100,7 @@ EE: true database records created during Cycle Analytics model spec." - _Any_ contribution from a community member, no matter how small, **may** have a changelog entry regardless of these guidelines if the contributor wants one. -- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry. +- Any [experiment](experiment_guide/index.md) changes **should not** have a changelog entry. - An MR that includes only documentation changes **should not** have a changelog entry. For more information, see diff --git a/doc/development/experiment_guide/experiment_code_reviews.md b/doc/development/experiment_guide/experiment_code_reviews.md new file mode 100644 index 00000000000..fdde89caa34 --- /dev/null +++ b/doc/development/experiment_guide/experiment_code_reviews.md @@ -0,0 +1,25 @@ +--- +stage: Growth +group: Adoption +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Experiment code reviews + +Experiments' code quality can fail our standards for several reasons. These +reasons can include not being added to the codebase for a long time, or because +of fast iteration to retrieve data. However, having the experiment run (or not +run) shouldn't impact GitLab availability. To avoid or identify issues, +experiments are initially deployed to a small number of users. Regardless, +experiments still need tests. + +Experiments must have corresponding [frontend or feature tests](../testing_guide/index.md) to ensure they +exist in the application. These tests should help prevent the experiment code from +being removed before the [experiment cleanup process](https://about.gitlab.com/handbook/engineering/development/growth/experimentation/#experiment-cleanup-issue) starts. + +If, as a reviewer or maintainer, you find code that would usually fail review +but is acceptable for now, mention your concerns with a note that there's no +need to change the code. The author can then add a comment to this piece of code +and link to the issue that resolves the experiment. The author or reviewer can add a link to this concern in the +experiment rollout issue under the `Experiment Successful Cleanup Concerns` section of the description. +If the experiment is successful and becomes part of the product, any items that appear under this section will be addressed. diff --git a/doc/development/experiment_guide/experiment_rollout.md b/doc/development/experiment_guide/experiment_rollout.md new file mode 100644 index 00000000000..bc700b13600 --- /dev/null +++ b/doc/development/experiment_guide/experiment_rollout.md @@ -0,0 +1,77 @@ +--- +stage: Growth +group: Adoption +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Experiment rollouts and feature flags + +## Experiment rollout issue + +Each experiment should have an [experiment rollout](https://gitlab.com/groups/gitlab-org/-/boards/1352542) issue to track the experiment from rollout through to cleanup and removal. +The rollout issue is similar to a feature flag rollout issue, and is also used to track the status of an experiment. + +When an experiment is deployed, the due date of the issue should be set (this depends on the experiment but can be up to a few weeks in the future). +After the deadline, the issue needs to be resolved and either: + +- It was successful and the experiment becomes the new default. +- It was not successful and all code related to the experiment is removed. + +In either case, an outcome of the experiment should be posted to the issue with the reasoning for the decision. + +## Turn off all experiments + +When there is a case on GitLab.com (SaaS) that necessitates turning off all experiments, we have this control. + +You can toggle experiments on SaaS on and off using the `gitlab_experiment` [feature flag](../feature_flags). + +This can be done via chatops: + +- [disable](../feature_flags/controls.md#disabling-feature-flags): `/chatops run feature set gitlab_experiment false` +- [enable](../feature_flags/controls.md#process): `/chatops run feature delete gitlab_experiment` +- This allows the `default_enabled` [value of true in the yml](https://gitlab.com/gitlab-org/gitlab/-/blob/016430f6751b0c34abb24f74608c80a1a8268f20/config/feature_flags/ops/gitlab_experiment.yml#L8) to be honored. + +## Notes on feature flags + +NOTE: +We use the terms "enabled" and "disabled" here, even though it's against our +[documentation style guide recommendations](../documentation/styleguide/word_list.md#enable) +because these are the terms that the feature flag documentation uses. + +You may already be familiar with the concept of feature flags in GitLab, but using +feature flags in experiments is a bit different. While in general terms, a feature flag +is viewed as being either `on` or `off`, this isn't accurate for experiments. + +Generally, `off` means that when we ask if a feature flag is enabled, it will always +return `false`, and `on` means that it will always return `true`. An interim state, +considered `conditional`, also exists. We take advantage of this trinary state of +feature flags. To understand this `conditional` aspect: consider that either of these +settings puts a feature flag into this state: + +- Setting a `percentage_of_actors` of any percent greater than 0%. +- Enabling it for a single user or group. + +Conditional means that it returns `true` in some situations, but not all situations. + +When a feature flag is disabled (meaning the state is `off`), the experiment is +considered _inactive_. You can visualize this in the [decision tree diagram](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment#how-it-works) +as reaching the first `Running?` node, and traversing the negative path. + +When a feature flag is rolled out to a `percentage_of_actors` or similar (meaning the +state is `conditional`) the experiment is considered to be _running_ +where sometimes the control is assigned, and sometimes the candidate is assigned. +We don't refer to this as being enabled, because that's a confusing and overloaded +term here. In the experiment terms, our experiment is _running_, and the feature flag is +`conditional`. + +When a feature flag is enabled (meaning the state is `on`), the candidate will always be +assigned. + +We should try to be consistent with our terms, and so for experiments, we have an +_inactive_ experiment until we set the feature flag to `conditional`. After which, +our experiment is then considered _running_. If you choose to "enable" your feature flag, +you should consider the experiment to be _resolved_, because everyone is assigned +the candidate unless they've opted out of experimentation. + +As of GitLab 13.10, work is being done to improve this process and how we communicate +about it. diff --git a/doc/development/experiment_guide/gitlab_experiment.md b/doc/development/experiment_guide/gitlab_experiment.md index 78e1f84d701..5ddbe9b3de9 100644 --- a/doc/development/experiment_guide/gitlab_experiment.md +++ b/doc/development/experiment_guide/gitlab_experiment.md @@ -1,586 +1,11 @@ --- -stage: Growth -group: Adoption -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +redirect_to: 'index.md' +remove_date: '2022-08-05' --- -# Implementing an A/B/n experiment +This document was moved to [another location](index.md). -## Introduction - -Experiments in GitLab are tightly coupled with the concepts provided by -[Feature flags in development of GitLab](../feature_flags/index.md). You're strongly encouraged -to read and understand the [Feature flags in development of GitLab](../feature_flags/index.md) -portion of the documentation before considering running experiments. Experiments add additional -concepts which may seem confusing or advanced without understanding the underpinnings of how GitLab -uses feature flags in development. One concept: experiments can be run with multiple variants, -which are sometimes referred to as A/B/n tests. - -We use the [`gitlab-experiment` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment), -sometimes referred to as GLEX, to run our experiments. The gem exists in a separate repository -so it can be shared across any GitLab property that uses Ruby. You should feel comfortable reading -the documentation on that project if you want to dig into more advanced topics or open issues. Be -aware that the documentation there reflects what's in the main branch and may not be the same as -the version being used within GitLab. - -## Glossary of terms - -To ensure a shared language, you should understand these fundamental terms we use -when communicating about experiments: - -- `experiment`: Any deviation of code paths we want to run at some times, but not others. -- `context`: A consistent experience we provide in an experiment. -- `control`: The default, or "original" code path. -- `candidate`: Defines an experiment with only one code path. -- `variant(s)`: Defines an experiment with multiple code paths. -- `behaviors`: Used to reference all possible code paths of an experiment, including the control. - -## Implementing an experiment - -[Examples](https://gitlab.com/gitlab-org/growth/growth/-/wikis/GLEX-Framework-code-examples) - -Start by generating a feature flag using the `bin/feature-flag` command as you -normally would for a development feature flag, making sure to use `experiment` for -the type. For the sake of documentation let's name our feature flag (and experiment) -"pill_color". - -```shell -bin/feature-flag pill_color -t experiment -``` - -After you generate the desired feature flag, you can immediately implement an -experiment in code. An experiment implementation can be as simple as: - -```ruby -experiment(:pill_color, actor: current_user) do |e| - e.control { 'control' } - e.variant(:red) { 'red' } - e.variant(:blue) { 'blue' } -end -``` - -When this code executes, the experiment is run, a variant is assigned, and (if within a -controller or view) a `window.gl.experiments.pill_color` object will be available in the -client layer, with details like: - -- The assigned variant. -- The context key for client tracking events. - -In addition, when an experiment runs, an event is tracked for -the experiment `:assignment`. We cover more about events, tracking, and -the client layer later. - -In local development, you can make the experiment active by using the feature flag -interface. You can also target specific cases by providing the relevant experiment -to the call to enable the feature flag: - -```ruby -# Enable for everyone -Feature.enable(:pill_color) - -# Get the `experiment` method -- already available in controllers, views, and mailers. -include Gitlab::Experiment::Dsl -# Enable for only the first user -Feature.enable(:pill_color, experiment(:pill_color, actor: User.first)) -``` - -To roll out your experiment feature flag on an environment, run -the following command using ChatOps (which is covered in more depth in the -[Feature flags in development of GitLab](../feature_flags/index.md) documentation). -This command creates a scenario where half of everyone who encounters -the experiment would be assigned the _control_, 25% would be assigned the _red_ -variant, and 25% would be assigned the _blue_ variant: - -```slack -/chatops run feature set pill_color 50 --actors -``` - -For an even distribution in this example, change the command to set it to 66% instead -of 50. - -NOTE: -To immediately stop running an experiment, use the -`/chatops run feature set pill_color false` command. - -WARNING: -We strongly recommend using the `--actors` flag when using the ChatOps commands, -as anything else may give odd behaviors due to how the caching of variant assignment is -handled. - -We can also implement this experiment in a HAML file with HTML wrappings: - -```haml -#cta-interface - - experiment(:pill_color, actor: current_user) do |e| - - e.control do - .pill-button control - - e.variant(:red) do - .pill-button.red red - - e.variant(:blue) do - .pill-button.blue blue -``` - -### The importance of context - -In our previous example experiment, our context (this is an important term) is a hash -that's set to `{ actor: current_user }`. Context must be unique based on how you -want to run your experiment, and should be understood at a lower level. - -It's expected, and recommended, that you use some of these -contexts to simplify reporting: - -- `{ actor: current_user }`: Assigns a variant and is "sticky" to each user - (or "client" if `current_user` is nil) who enters the experiment. -- `{ project: project }`: Assigns a variant and is "sticky" to the project currently - being viewed. If running your experiment is more useful when viewing a project, - rather than when a specific user is viewing any project, consider this approach. -- `{ group: group }`: Similar to the project example, but applies to a wider - scope of projects and users. -- `{ actor: current_user, project: project }`: Assigns a variant and is "sticky" - to the user who is viewing the given project. This creates a different variant - assignment possibility for every project that `current_user` views. Understand this - can create a large cache size if an experiment like this in a highly trafficked part - of the application. -- `{ wday: Time.current.wday }`: Assigns a variant based on the current day of the - week. In this example, it would consistently assign one variant on Friday, and a - potentially different variant on Saturday. - -Context is critical to how you define and report on your experiment. It's usually -the most important aspect of how you choose to implement your experiment, so consider -it carefully, and discuss it with the wider team if needed. Also, take into account -that the context you choose affects our cache size. - -After the above examples, we can state the general case: *given a specific -and consistent context, we can provide a consistent experience and track events for -that experience.* To dive a bit deeper into the implementation details: a context key -is generated from the context that's provided. Use this context key to: - -- Determine the assigned variant. -- Identify events tracked against that context key. - -We can think about this as the experience that we've rendered, which is both dictated -and tracked by the context key. The context key is used to track the interaction and -results of the experience we've rendered to that context key. These concepts are -somewhat abstract and hard to understand initially, but this approach enables us to -communicate about experiments as something that's wider than just user behavior. - -NOTE: -Using `actor:` utilizes cookies if the `current_user` is nil. If you don't need -cookies though - meaning that the exposed functionality would only be visible to -signed in users - `{ user: current_user }` would be just as effective. - -WARNING: -The caching of variant assignment is done by using this context, and so consider -your impact on the cache size when defining your experiment. If you use -`{ time: Time.current }` you would be inflating the cache size every time the -experiment is run. Not only that, your experiment would not be "sticky" and events -wouldn't be resolvable. - -### Advanced experimentation - -There are two ways to implement an experiment: - -1. The simple experiment style described previously. -1. A more advanced style where an experiment class is provided. - -The advanced style is handled by naming convention, and works similar to what you -would expect in Rails. - -To generate a custom experiment class that can override the defaults in -`ApplicationExperiment` use the Rails generator: - -```shell -rails generate gitlab:experiment pill_color control red blue -``` - -This generates an experiment class in `app/experiments/pill_color_experiment.rb` -with the _behaviors_ we've provided to the generator. Here's an example -of how that class would look after migrating our previous example into it: - -```ruby -class PillColorExperiment < ApplicationExperiment - control { 'control' } - variant(:red) { 'red' } - variant(:blue) { 'blue' } -end -``` - -We can now simplify where we run our experiment to the following call, instead of -providing the block we were initially providing, by explicitly calling `run`: - -```ruby -experiment(:pill_color, actor: current_user).run -``` - -The _behaviors_ we defined in our experiment class represent the default -implementation. You can still use the block syntax to override these _behaviors_ -however, so the following would also be valid: - -```ruby -experiment(:pill_color, actor: current_user) do |e| - e.control { '<strong>control</strong>' } -end -``` - -NOTE: -When passing a block to the `experiment` method, it is implicitly invoked as -if `run` has been called. - -#### Segmentation rules - -You can use runtime segmentation rules to, for instance, segment contexts into a specific -variant. The `segment` method is a callback (like `before_action`) and so allows providing -a block or method name. - -In this example, any user named `'Richard'` would always be assigned the _red_ -variant, and any account older than 2 weeks old would be assigned the _blue_ variant: - -```ruby -class PillColorExperiment < ApplicationExperiment - # ...registered behaviors - - segment(variant: :red) { context.actor.first_name == 'Richard' } - segment :old_account?, variant: :blue - - private - - def old_account? - context.actor.created_at < 2.weeks.ago - end -end -``` - -When an experiment runs, the segmentation rules are executed in the order they're -defined. The first segmentation rule to produce a truthy result assigns the variant. - -In our example, any user named `'Richard'`, regardless of account age, will always -be assigned the _red_ variant. If you want the opposite logic, flip the order. - -NOTE: -Keep in mind when defining segmentation rules: after a truthy result, the remaining -segmentation rules are skipped to achieve optimal performance. - -#### Exclusion rules - -Exclusion rules are similar to segmentation rules, but are intended to determine -if a context should even be considered as something we should include in the experiment -and track events toward. Exclusion means we don't care about the events in relation -to the given context. - -These examples exclude all users named `'Richard'`, *and* any account -older than 2 weeks old. Not only are they given the control behavior - which could -be nothing - but no events are tracked in these cases as well. - -```ruby -class PillColorExperiment < ApplicationExperiment - # ...registered behaviors - - exclude :old_account?, ->{ context.actor.first_name == 'Richard' } - - private - - def old_account? - context.actor.created_at < 2.weeks.ago - end -end -``` - -You may also need to check exclusion in custom tracking logic by calling `should_track?`: - -```ruby -class PillColorExperiment < ApplicationExperiment - # ...registered behaviors - - def expensive_tracking_logic - return unless should_track? - - track(:my_event, value: expensive_method_call) - end -end -``` - -### Tracking events - -One of the most important aspects of experiments is gathering data and reporting on -it. You can use the `track` method to track events across an experimental implementation. -You can track events consistently to an experiment if you provide the same context between -calls to your experiment. If you do not yet understand context, you should read -about contexts now. - -We can assume we run the experiment in one or a few places, but -track events potentially in many places. The tracking call remains the same, with -the arguments you would normally use when -[tracking events using snowplow](../snowplow/index.md). The easiest example -of tracking an event in Ruby would be: - -```ruby -experiment(:pill_color, actor: current_user).track(:clicked) -``` - -When you run an experiment with any of the examples so far, an `:assignment` event -is tracked automatically by default. All events that are tracked from an -experiment have a special -[experiment context](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3) -added to the event. This can be used - typically by the data team - to create a connection -between the events on a given experiment. - -If our current user hasn't encountered the experiment yet (meaning where the experiment -is run), and we track an event for them, they are assigned a variant and see -that variant if they ever encountered the experiment later, when an `:assignment` -event would be tracked at that time for them. - -NOTE: -GitLab tries to be sensitive and respectful of our customers regarding tracking, -so our experimentation library allows us to implement an experiment without ever tracking identifying -IDs. It's not always possible, though, based on experiment reporting requirements. -You may be asked from time to time to track a specific record ID in experiments. -The approach is largely up to the PM and engineer creating the implementation. -No recommendations are provided here at this time. - -## Testing with RSpec - -In the course of working with experiments, you'll probably want to utilize the RSpec -tooling that's built in. This happens automatically for files in `spec/experiments`, but -for other files and specs you want to include it in, you can specify the `:experiment` type: - -```ruby -it "tests experiments nicely", :experiment do -end -``` - -### Stub helpers - -You can stub experiments using `stub_experiments`. Pass it a hash using experiment -names as the keys, and the variants you want each to resolve to, as the values: - -```ruby -# Ensures the experiments named `:example` & `:example2` are both "enabled" and -# that each will resolve to the given variant (`:my_variant` and `:control` -# respectively). -stub_experiments(example: :my_variant, example2: :control) - -experiment(:example) do |e| - e.enabled? # => true - e.assigned.name # => 'my_variant' -end - -experiment(:example2) do |e| - e.enabled? # => true - e.assigned.name # => 'control' -end -``` - -### Exclusion, segmentation, and behavior matchers - -You can also test things like the registered behaviors, the exclusions, and -segmentations using the matchers. - -```ruby -class ExampleExperiment < ApplicationExperiment - control { } - candidate { '_candidate_' } - - exclude { context.actor.first_name == 'Richard' } - segment(variant: :candidate) { context.actor.username == 'jejacks0n' } -end - -excluded = double(username: 'rdiggitty', first_name: 'Richard') -segmented = double(username: 'jejacks0n', first_name: 'Jeremy') - -# register_behavior matcher -expect(experiment(:example)).to register_behavior(:control) -expect(experiment(:example)).to register_behavior(:candidate).with('_candidate_') - -# exclude matcher -expect(experiment(:example)).to exclude(actor: excluded) -expect(experiment(:example)).not_to exclude(actor: segmented) - -# segment matcher -expect(experiment(:example)).to segment(actor: segmented).into(:candidate) -expect(experiment(:example)).not_to segment(actor: excluded) -``` - -### Tracking matcher - -Tracking events is a major aspect of experimentation. We try -to provide a flexible way to ensure your tracking calls are covered. - -You can do this on the instance level or at an "any instance" level: - -```ruby -subject = experiment(:example) - -expect(subject).to track(:my_event) - -subject.track(:my_event) -``` - -You can use the `on_next_instance` chain method to specify that it will happen -on the next instance of the experiment. This helps you if you're calling -`experiment(:example).track` downstream: - -```ruby -expect(experiment(:example)).to track(:my_event).on_next_instance - -experiment(:example).track(:my_event) -``` - -A full example of the methods you can chain onto the `track` matcher: - -```ruby -expect(experiment(:example)).to track(:my_event, value: 1, property: '_property_') - .on_next_instance - .with_context(foo: :bar) - .for(:variant_name) - -experiment(:example, :variant_name, foo: :bar).track(:my_event, value: 1, property: '_property_') -``` - -## Experiments in the client layer - -Any experiment that's been run in the request lifecycle surfaces in `window.gl.experiments`, -and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3) -so it can be used when resolving experimentation in the client layer. - -Given that we've defined a class for our experiment, and have defined the variants for it, we can publish that experiment in a couple ways. - -The first way is simply by running the experiment. Assuming the experiment has been run, it will surface in the client layer without having to do anything special. - -The second way doesn't run the experiment and is intended to be used if the experiment only needs to surface in the client layer. To accomplish this we can simply `.publish` the experiment. This won't run any logic, but does surface the experiment details in the client layer so they can be utilized there. - -An example might be to publish an experiment in a `before_action` in a controller. Assuming we've defined the `PillColorExperiment` class, like we have above, we can surface it to the client by publishing it instead of running it: - -```ruby -before_action -> { experiment(:pill_color).publish }, only: [:show] -``` - -You can then see this surface in the JavaScript console: - -```javascript -window.gl.experiments // => { pill_color: { excluded: false, experiment: "pill_color", key: "ca63ac02", variant: "candidate" } } -``` - -### Using experiments in Vue - -With the `gitlab-experiment` component, you can define slots that match the name of the -variants pushed to `window.gl.experiments`. - -We can make use of the named slots in the Vue component, that match the behaviors defined in : - -```vue -<script> -import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; - -export default { - components: { GitlabExperiment } -} -</script> - -<template> - <gitlab-experiment name="pill_color"> - <template #control> - <button class="bg-default">Click default button</button> - </template> - - <template #red> - <button class="bg-red">Click red button</button> - </template> - - <template #blue> - <button class="bg-blue">Click blue button</button> - </template> - </gitlab-experiment> -</template> -``` - -NOTE: -When there is no experiment data in the `window.gl.experiments` object for the given experiment name, the `control` slot will be used, if it exists. - -## Test with Jest - -### Stub Helpers - -You can stub experiments using the `stubExperiments` helper defined in `spec/frontend/__helpers__/experimentation_helper.js`. - -```javascript -import { stubExperiments } from 'helpers/experimentation_helper'; -import { getExperimentData } from '~/experimentation/utils'; - -describe('when my_experiment is enabled', () => { - beforeEach(() => { - stubExperiments({ my_experiment: 'candidate' }); - }); - - it('sets the correct data', () => { - expect(getExperimentData('my_experiment')).toEqual({ experiment: 'my_experiment', variant: 'candidate' }); - }); -}); -``` - -NOTE: -This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself: - -```javascript -describe('tests that care about global state', () => { - const originalObjects = []; - - beforeEach(() => { - // For backwards compatibility for now, we're using both window.gon & window.gl - originalObjects.push(window.gon, window.gl); - }); - - afterEach(() => { - [window.gon, window.gl] = originalObjects; - }); - - it('stubs experiment in fresh global state', () => { - stubExperiment({ my_experiment: 'candidate' }); - // ... - }); -}) -``` - -## Notes on feature flags - -NOTE: -We use the terms "enabled" and "disabled" here, even though it's against our -[documentation style guide recommendations](../documentation/styleguide/word_list.md#enable) -because these are the terms that the feature flag documentation uses. - -You may already be familiar with the concept of feature flags in GitLab, but using -feature flags in experiments is a bit different. While in general terms, a feature flag -is viewed as being either `on` or `off`, this isn't accurate for experiments. - -Generally, `off` means that when we ask if a feature flag is enabled, it will always -return `false`, and `on` means that it will always return `true`. An interim state, -considered `conditional`, also exists. We take advantage of this trinary state of -feature flags. To understand this `conditional` aspect: consider that either of these -settings puts a feature flag into this state: - -- Setting a `percentage_of_actors` of any percent greater than 0%. -- Enabling it for a single user or group. - -Conditional means that it returns `true` in some situations, but not all situations. - -When a feature flag is disabled (meaning the state is `off`), the experiment is -considered _inactive_. You can visualize this in the [decision tree diagram](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment#how-it-works) -as reaching the first `Running?` node, and traversing the negative path. - -When a feature flag is rolled out to a `percentage_of_actors` or similar (meaning the -state is `conditional`) the experiment is considered to be _running_ -where sometimes the control is assigned, and sometimes the candidate is assigned. -We don't refer to this as being enabled, because that's a confusing and overloaded -term here. In the experiment terms, our experiment is _running_, and the feature flag is -`conditional`. - -When a feature flag is enabled (meaning the state is `on`), the candidate will always be -assigned. - -We should try to be consistent with our terms, and so for experiments, we have an -_inactive_ experiment until we set the feature flag to `conditional`. After which, -our experiment is then considered _running_. If you choose to "enable" your feature flag, -you should consider the experiment to be _resolved_, because everyone is assigned -the candidate unless they've opted out of experimentation. - -As of GitLab 13.10, work is being done to improve this process and how we communicate -about it. +<!-- This redirect file can be deleted after 2022-08-05. --> +<!-- Redirects that point to other docs in the same project expire in three months. --> +<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> diff --git a/doc/development/experiment_guide/implementing_experiments.md b/doc/development/experiment_guide/implementing_experiments.md new file mode 100644 index 00000000000..3c33d015108 --- /dev/null +++ b/doc/development/experiment_guide/implementing_experiments.md @@ -0,0 +1,369 @@ +--- +stage: Growth +group: Adoption +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Implementing an A/B/n experiment + +## Implementing an experiment + +[Examples](https://gitlab.com/gitlab-org/growth/growth/-/wikis/GLEX-Framework-code-examples) + +Start by generating a feature flag using the `bin/feature-flag` command as you +normally would for a development feature flag, making sure to use `experiment` for +the type. For the sake of documentation let's name our feature flag (and experiment) +"pill_color". + +```shell +bin/feature-flag pill_color -t experiment +``` + +After you generate the desired feature flag, you can immediately implement an +experiment in code. An experiment implementation can be as simple as: + +```ruby +experiment(:pill_color, actor: current_user) do |e| + e.control { 'control' } + e.variant(:red) { 'red' } + e.variant(:blue) { 'blue' } +end +``` + +When this code executes, the experiment is run, a variant is assigned, and (if within a +controller or view) a `window.gl.experiments.pill_color` object will be available in the +client layer, with details like: + +- The assigned variant. +- The context key for client tracking events. + +In addition, when an experiment runs, an event is tracked for +the experiment `:assignment`. We cover more about events, tracking, and +the client layer later. + +In local development, you can make the experiment active by using the feature flag +interface. You can also target specific cases by providing the relevant experiment +to the call to enable the feature flag: + +```ruby +# Enable for everyone +Feature.enable(:pill_color) + +# Get the `experiment` method -- already available in controllers, views, and mailers. +include Gitlab::Experiment::Dsl +# Enable for only the first user +Feature.enable(:pill_color, experiment(:pill_color, actor: User.first)) +``` + +To roll out your experiment feature flag on an environment, run +the following command using ChatOps (which is covered in more depth in the +[Feature flags in development of GitLab](../feature_flags/index.md) documentation). +This command creates a scenario where half of everyone who encounters +the experiment would be assigned the _control_, 25% would be assigned the _red_ +variant, and 25% would be assigned the _blue_ variant: + +```slack +/chatops run feature set pill_color 50 --actors +``` + +For an even distribution in this example, change the command to set it to 66% instead +of 50. + +NOTE: +To immediately stop running an experiment, use the +`/chatops run feature set pill_color false` command. + +WARNING: +We strongly recommend using the `--actors` flag when using the ChatOps commands, +as anything else may give odd behaviors due to how the caching of variant assignment is +handled. + +We can also implement this experiment in a HAML file with HTML wrappings: + +```haml +#cta-interface + - experiment(:pill_color, actor: current_user) do |e| + - e.control do + .pill-button control + - e.variant(:red) do + .pill-button.red red + - e.variant(:blue) do + .pill-button.blue blue +``` + +### The importance of context + +In our previous example experiment, our context (this is an important term) is a hash +that's set to `{ actor: current_user }`. Context must be unique based on how you +want to run your experiment, and should be understood at a lower level. + +It's expected, and recommended, that you use some of these +contexts to simplify reporting: + +- `{ actor: current_user }`: Assigns a variant and is "sticky" to each user + (or "client" if `current_user` is nil) who enters the experiment. +- `{ project: project }`: Assigns a variant and is "sticky" to the project currently + being viewed. If running your experiment is more useful when viewing a project, + rather than when a specific user is viewing any project, consider this approach. +- `{ group: group }`: Similar to the project example, but applies to a wider + scope of projects and users. +- `{ actor: current_user, project: project }`: Assigns a variant and is "sticky" + to the user who is viewing the given project. This creates a different variant + assignment possibility for every project that `current_user` views. Understand this + can create a large cache size if an experiment like this in a highly trafficked part + of the application. +- `{ wday: Time.current.wday }`: Assigns a variant based on the current day of the + week. In this example, it would consistently assign one variant on Friday, and a + potentially different variant on Saturday. + +Context is critical to how you define and report on your experiment. It's usually +the most important aspect of how you choose to implement your experiment, so consider +it carefully, and discuss it with the wider team if needed. Also, take into account +that the context you choose affects our cache size. + +After the above examples, we can state the general case: *given a specific +and consistent context, we can provide a consistent experience and track events for +that experience.* To dive a bit deeper into the implementation details: a context key +is generated from the context that's provided. Use this context key to: + +- Determine the assigned variant. +- Identify events tracked against that context key. + +We can think about this as the experience that we've rendered, which is both dictated +and tracked by the context key. The context key is used to track the interaction and +results of the experience we've rendered to that context key. These concepts are +somewhat abstract and hard to understand initially, but this approach enables us to +communicate about experiments as something that's wider than just user behavior. + +NOTE: +Using `actor:` utilizes cookies if the `current_user` is nil. If you don't need +cookies though - meaning that the exposed functionality would only be visible to +signed in users - `{ user: current_user }` would be just as effective. + +WARNING: +The caching of variant assignment is done by using this context, and so consider +your impact on the cache size when defining your experiment. If you use +`{ time: Time.current }` you would be inflating the cache size every time the +experiment is run. Not only that, your experiment would not be "sticky" and events +wouldn't be resolvable. + +### Advanced experimentation + +There are two ways to implement an experiment: + +1. The simple experiment style described previously. +1. A more advanced style where an experiment class is provided. + +The advanced style is handled by naming convention, and works similar to what you +would expect in Rails. + +To generate a custom experiment class that can override the defaults in +`ApplicationExperiment` use the Rails generator: + +```shell +rails generate gitlab:experiment pill_color control red blue +``` + +This generates an experiment class in `app/experiments/pill_color_experiment.rb` +with the _behaviors_ we've provided to the generator. Here's an example +of how that class would look after migrating our previous example into it: + +```ruby +class PillColorExperiment < ApplicationExperiment + control { 'control' } + variant(:red) { 'red' } + variant(:blue) { 'blue' } +end +``` + +We can now simplify where we run our experiment to the following call, instead of +providing the block we were initially providing, by explicitly calling `run`: + +```ruby +experiment(:pill_color, actor: current_user).run +``` + +The _behaviors_ we defined in our experiment class represent the default +implementation. You can still use the block syntax to override these _behaviors_ +however, so the following would also be valid: + +```ruby +experiment(:pill_color, actor: current_user) do |e| + e.control { '<strong>control</strong>' } +end +``` + +NOTE: +When passing a block to the `experiment` method, it is implicitly invoked as +if `run` has been called. + +#### Segmentation rules + +You can use runtime segmentation rules to, for instance, segment contexts into a specific +variant. The `segment` method is a callback (like `before_action`) and so allows providing +a block or method name. + +In this example, any user named `'Richard'` would always be assigned the _red_ +variant, and any account older than 2 weeks old would be assigned the _blue_ variant: + +```ruby +class PillColorExperiment < ApplicationExperiment + # ...registered behaviors + + segment(variant: :red) { context.actor.first_name == 'Richard' } + segment :old_account?, variant: :blue + + private + + def old_account? + context.actor.created_at < 2.weeks.ago + end +end +``` + +When an experiment runs, the segmentation rules are executed in the order they're +defined. The first segmentation rule to produce a truthy result assigns the variant. + +In our example, any user named `'Richard'`, regardless of account age, will always +be assigned the _red_ variant. If you want the opposite logic, flip the order. + +NOTE: +Keep in mind when defining segmentation rules: after a truthy result, the remaining +segmentation rules are skipped to achieve optimal performance. + +#### Exclusion rules + +Exclusion rules are similar to segmentation rules, but are intended to determine +if a context should even be considered as something we should include in the experiment +and track events toward. Exclusion means we don't care about the events in relation +to the given context. + +These examples exclude all users named `'Richard'`, *and* any account +older than 2 weeks old. Not only are they given the control behavior - which could +be nothing - but no events are tracked in these cases as well. + +```ruby +class PillColorExperiment < ApplicationExperiment + # ...registered behaviors + + exclude :old_account?, ->{ context.actor.first_name == 'Richard' } + + private + + def old_account? + context.actor.created_at < 2.weeks.ago + end +end +``` + +You may also need to check exclusion in custom tracking logic by calling `should_track?`: + +```ruby +class PillColorExperiment < ApplicationExperiment + # ...registered behaviors + + def expensive_tracking_logic + return unless should_track? + + track(:my_event, value: expensive_method_call) + end +end +``` + +### Tracking events + +One of the most important aspects of experiments is gathering data and reporting on +it. You can use the `track` method to track events across an experimental implementation. +You can track events consistently to an experiment if you provide the same context between +calls to your experiment. If you do not yet understand context, you should read +about contexts now. + +We can assume we run the experiment in one or a few places, but +track events potentially in many places. The tracking call remains the same, with +the arguments you would normally use when +[tracking events using snowplow](../snowplow/index.md). The easiest example +of tracking an event in Ruby would be: + +```ruby +experiment(:pill_color, actor: current_user).track(:clicked) +``` + +When you run an experiment with any of the examples so far, an `:assignment` event +is tracked automatically by default. All events that are tracked from an +experiment have a special +[experiment context](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3) +added to the event. This can be used - typically by the data team - to create a connection +between the events on a given experiment. + +If our current user hasn't encountered the experiment yet (meaning where the experiment +is run), and we track an event for them, they are assigned a variant and see +that variant if they ever encountered the experiment later, when an `:assignment` +event would be tracked at that time for them. + +NOTE: +GitLab tries to be sensitive and respectful of our customers regarding tracking, +so our experimentation library allows us to implement an experiment without ever tracking identifying +IDs. It's not always possible, though, based on experiment reporting requirements. +You may be asked from time to time to track a specific record ID in experiments. +The approach is largely up to the PM and engineer creating the implementation. +No recommendations are provided here at this time. + +## Experiments in the client layer + +Any experiment that's been run in the request lifecycle surfaces in `window.gl.experiments`, +and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3) +so it can be used when resolving experimentation in the client layer. + +Given that we've defined a class for our experiment, and have defined the variants for it, we can publish that experiment in a couple ways. + +The first way is simply by running the experiment. Assuming the experiment has been run, it will surface in the client layer without having to do anything special. + +The second way doesn't run the experiment and is intended to be used if the experiment only needs to surface in the client layer. To accomplish this we can simply `.publish` the experiment. This won't run any logic, but does surface the experiment details in the client layer so they can be utilized there. + +An example might be to publish an experiment in a `before_action` in a controller. Assuming we've defined the `PillColorExperiment` class, like we have above, we can surface it to the client by publishing it instead of running it: + +```ruby +before_action -> { experiment(:pill_color).publish }, only: [:show] +``` + +You can then see this surface in the JavaScript console: + +```javascript +window.gl.experiments // => { pill_color: { excluded: false, experiment: "pill_color", key: "ca63ac02", variant: "candidate" } } +``` + +### Using experiments in Vue + +With the `gitlab-experiment` component, you can define slots that match the name of the +variants pushed to `window.gl.experiments`. + +We can make use of the named slots in the Vue component, that match the behaviors defined in : + +```vue +<script> +import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; + +export default { + components: { GitlabExperiment } +} +</script> + +<template> + <gitlab-experiment name="pill_color"> + <template #control> + <button class="bg-default">Click default button</button> + </template> + + <template #red> + <button class="bg-red">Click red button</button> + </template> + + <template #blue> + <button class="bg-blue">Click blue button</button> + </template> + </gitlab-experiment> +</template> +``` + +NOTE: +When there is no experiment data in the `window.gl.experiments` object for the given experiment name, the `control` slot will be used, if it exists. diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md index f7af1113b6e..b140cce34fc 100644 --- a/doc/development/experiment_guide/index.md +++ b/doc/development/experiment_guide/index.md @@ -6,47 +6,46 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Experiment Guide -Experiments can be conducted by any GitLab team, most often the teams from the [Growth Sub-department](https://about.gitlab.com/handbook/engineering/development/growth/). Experiments are not tied to releases because they primarily target GitLab.com. - -Experiments are run as an A/B/n test, and are behind an [experiment feature flag](../feature_flags/#experiment-type) to turn the test on or off. Based on the data the experiment generates, the team decides if the experiment had a positive impact and should be made the new default, or rolled back. - -## Experiment rollout issue - -Each experiment should have an [experiment rollout](https://gitlab.com/groups/gitlab-org/-/boards/1352542) issue to track the experiment from rollout through to cleanup and removal. -The rollout issue is similar to a feature flag rollout issue, and is also used to track the status of an experiment. -When an experiment is deployed, the due date of the issue should be set (this depends on the experiment but can be up to a few weeks in the future). -After the deadline, the issue needs to be resolved and either: - -- It was successful and the experiment becomes the new default. -- It was not successful and all code related to the experiment is removed. - -In either case, an outcome of the experiment should be posted to the issue with the reasoning for the decision. - -## Code reviews - -Experiments' code quality can fail our standards for several reasons. These -reasons can include not being added to the codebase for a long time, or because -of fast iteration to retrieve data. However, having the experiment run (or not -run) shouldn't impact GitLab availability. To avoid or identify issues, -experiments are initially deployed to a small number of users. Regardless, -experiments still need tests. - -Experiments must have corresponding [frontend or feature tests](../testing_guide/index.md) to ensure they -exist in the application. These tests should help prevent the experiment code from -being removed before the [experiment cleanup process](https://about.gitlab.com/handbook/engineering/development/growth/experimentation/#experiment-cleanup-issue) starts. - -If, as a reviewer or maintainer, you find code that would usually fail review -but is acceptable for now, mention your concerns with a note that there's no -need to change the code. The author can then add a comment to this piece of code -and link to the issue that resolves the experiment. The author or reviewer can add a link to this concern in the -experiment rollout issue under the `Experiment Successful Cleanup Concerns` section of the description. -If the experiment is successful and becomes part of the product, any items that appear under this section will be addressed. +Experiments can be conducted by any GitLab team, most often the teams from the +[Growth Sub-department](https://about.gitlab.com/handbook/engineering/development/growth/). +Experiments are not tied to releases because they primarily target GitLab.com. + +Experiments are run as an A/B/n test, and are behind an [experiment feature flag](../feature_flags/#experiment-type) +to turn the test on or off. Based on the data the experiment generates, the team decides +if the experiment had a positive impact and should be made the new default, or rolled back. + +Experiments in GitLab are tightly coupled with the concepts provided by +[Feature flags in development of GitLab](../feature_flags/index.md). You're strongly encouraged +to read and understand the [Feature flags in development of GitLab](../feature_flags/index.md) +portion of the documentation before considering running experiments. Experiments add additional +concepts which may seem confusing or advanced without understanding the underpinnings of how GitLab +uses feature flags in development. One concept: experiments can be run with multiple variants, +which are sometimes referred to as A/B/n tests. + +We use the [`gitlab-experiment` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment), +sometimes referred to as GLEX, to run our experiments. The gem exists in a separate repository +so it can be shared across any GitLab property that uses Ruby. You should feel comfortable reading +the documentation on that project if you want to dig into more advanced topics or open issues. Be +aware that the documentation there reflects what's in the main branch and may not be the same as +the version being used within GitLab. + +## Glossary of terms + +To ensure a shared language, you should understand these fundamental terms we use +when communicating about experiments: + +- `experiment`: Any deviation of code paths we want to run at some times, but not others. +- `context`: A consistent experience we provide in an experiment. +- `control`: The default, or "original" code path. +- `candidate`: Defines an experiment with only one code path. +- `variant(s)`: Defines an experiment with multiple code paths. +- `behaviors`: Used to reference all possible code paths of an experiment, including the control. ## Implementing an experiment [`GLEX`](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment) - or `Gitlab::Experiment`, the `gitlab-experiment` gem - is the preferred option for implementing an experiment in GitLab. -For more information, see [Implementing an A/B/n experiment using GLEX](gitlab_experiment.md). +For more information, see [Implementing an A/B/n experiment using GLEX](implementing_experiments.md). This uses [experiment](../feature_flags/index.md#experiment-type) feature flags. @@ -64,15 +63,3 @@ We recommend the following workflow: 1. **If the experiment is a success**, designers add the new icon or illustration to the Pajamas UI kit as part of the cleanup process. Engineers can then add it to the [SVG library](https://gitlab-org.gitlab.io/gitlab-svgs/) and modify the implementation based on the [Frontend Development Guidelines](../fe_guide/icons.md#usage-in-hamlrails-2). - -## Turn off all experiments - -When there is a case on GitLab.com (SaaS) that necessitates turning off all experiments, we have this control. - -You can toggle experiments on SaaS on and off using the `gitlab_experiment` [feature flag](../feature_flags). - -This can be done via chatops: - -- [disable](../feature_flags/controls.md#disabling-feature-flags): `/chatops run feature set gitlab_experiment false` -- [enable](../feature_flags/controls.md#process): `/chatops run feature delete gitlab_experiment` - - This allows the `default_enabled` [value of true in the yml](https://gitlab.com/gitlab-org/gitlab/-/blob/016430f6751b0c34abb24f74608c80a1a8268f20/config/feature_flags/ops/gitlab_experiment.yml#L8) to be honored. diff --git a/doc/development/experiment_guide/testing_experiments.md b/doc/development/experiment_guide/testing_experiments.md new file mode 100644 index 00000000000..c51c3c7cb11 --- /dev/null +++ b/doc/development/experiment_guide/testing_experiments.md @@ -0,0 +1,150 @@ +--- +stage: Growth +group: Activation +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Testing experiments + +## Testing experiments with RSpec + +In the course of working with experiments, you'll probably want to utilize the RSpec +tooling that's built in. This happens automatically for files in `spec/experiments`, but +for other files and specs you want to include it in, you can specify the `:experiment` type: + +```ruby +it "tests experiments nicely", :experiment do +end +``` + +### Stub helpers + +You can stub experiments using `stub_experiments`. Pass it a hash using experiment +names as the keys, and the variants you want each to resolve to, as the values: + +```ruby +# Ensures the experiments named `:example` & `:example2` are both "enabled" and +# that each will resolve to the given variant (`:my_variant` and `:control` +# respectively). +stub_experiments(example: :my_variant, example2: :control) + +experiment(:example) do |e| + e.enabled? # => true + e.assigned.name # => 'my_variant' +end + +experiment(:example2) do |e| + e.enabled? # => true + e.assigned.name # => 'control' +end +``` + +### Exclusion, segmentation, and behavior matchers + +You can also test things like the registered behaviors, the exclusions, and +segmentations using the matchers. + +```ruby +class ExampleExperiment < ApplicationExperiment + control { } + candidate { '_candidate_' } + + exclude { context.actor.first_name == 'Richard' } + segment(variant: :candidate) { context.actor.username == 'jejacks0n' } +end + +excluded = double(username: 'rdiggitty', first_name: 'Richard') +segmented = double(username: 'jejacks0n', first_name: 'Jeremy') + +# register_behavior matcher +expect(experiment(:example)).to register_behavior(:control) +expect(experiment(:example)).to register_behavior(:candidate).with('_candidate_') + +# exclude matcher +expect(experiment(:example)).to exclude(actor: excluded) +expect(experiment(:example)).not_to exclude(actor: segmented) + +# segment matcher +expect(experiment(:example)).to segment(actor: segmented).into(:candidate) +expect(experiment(:example)).not_to segment(actor: excluded) +``` + +### Tracking matcher + +Tracking events is a major aspect of experimentation. We try +to provide a flexible way to ensure your tracking calls are covered. + +You can do this on the instance level or at an "any instance" level: + +```ruby +subject = experiment(:example) + +expect(subject).to track(:my_event) + +subject.track(:my_event) +``` + +You can use the `on_next_instance` chain method to specify that it will happen +on the next instance of the experiment. This helps you if you're calling +`experiment(:example).track` downstream: + +```ruby +expect(experiment(:example)).to track(:my_event).on_next_instance + +experiment(:example).track(:my_event) +``` + +A full example of the methods you can chain onto the `track` matcher: + +```ruby +expect(experiment(:example)).to track(:my_event, value: 1, property: '_property_') + .on_next_instance + .with_context(foo: :bar) + .for(:variant_name) + +experiment(:example, :variant_name, foo: :bar).track(:my_event, value: 1, property: '_property_') +``` + +## Test with Jest + +### Stub Helpers + +You can stub experiments using the `stubExperiments` helper defined in `spec/frontend/__helpers__/experimentation_helper.js`. + +```javascript +import { stubExperiments } from 'helpers/experimentation_helper'; +import { getExperimentData } from '~/experimentation/utils'; + +describe('when my_experiment is enabled', () => { + beforeEach(() => { + stubExperiments({ my_experiment: 'candidate' }); + }); + + it('sets the correct data', () => { + expect(getExperimentData('my_experiment')).toEqual({ experiment: 'my_experiment', variant: 'candidate' }); + }); +}); +``` + +NOTE: +This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself: + +```javascript +describe('tests that care about global state', () => { + const originalObjects = []; + + beforeEach(() => { + // For backwards compatibility for now, we're using both window.gon & window.gl + originalObjects.push(window.gon, window.gl); + }); + + afterEach(() => { + [window.gon, window.gl] = originalObjects; + }); + + it('stubs experiment in fresh global state', () => { + stubExperiment({ my_experiment: 'candidate' }); + // ... + }); +}) +``` diff --git a/doc/update/index.md b/doc/update/index.md index 6558a289cdc..dcda9150059 100644 --- a/doc/update/index.md +++ b/doc/update/index.md @@ -193,6 +193,8 @@ pending_job_classes.each { |job_class| Gitlab::BackgroundMigration.steal(job_cla GitLab 13.6 introduced an issue where a background migration named `BackfillJiraTrackerDeploymentType2` can be permanently stuck in a **pending** state across upgrades. To clean up this stuck migration, see the [13.6.0 version-specific instructions](#1360). +GitLab 14.2 introduced an issue where a background migration named `BackfillDraftStatusOnMergeRequests` can be permanently stuck in a **pending** state across upgrades when the instance lacks records that match the migration's target. To clean up this stuck migration, see the [14.2.0 version-specific instructions](#1420). + GitLab 14.4 introduced an issue where a background migration named `PopulateTopicsTotalProjectsCountCache` can be permanently stuck in a **pending** state across upgrades when the instance lacks records that match the migration's target. To clean up this stuck migration, see the [14.4.0 version-specific instructions](#1440). GitLab 14.8 introduced an issue where a background migration named `PopulateTopicsNonPrivateProjectsCount` can be permanently stuck in a **pending** state across upgrades. To clean up this stuck migration, see the [14.8.0 version-specific instructions](#1480). @@ -616,6 +618,17 @@ for how to proceed. ``` - See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144). +- GitLab 14.2.0 includes a + [background migration `BackfillDraftStatusOnMergeRequests`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67687) + that may remain stuck permanently in a **pending** state when the instance lacks records that match the migration's target. + + To clean up this stuck job, run the following in the [GitLab Rails Console](../administration/operations/rails_console.md): + + ```ruby + Gitlab::Database::BackgroundMigrationJob.pending.where(class_name: "BackfillDraftStatusOnMergeRequests").find_each do |job| + puts Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded("BackfillDraftStatusOnMergeRequests", job.arguments) + end + ``` ### 14.1.0 diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 64566e458ee..006c9902859 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -217,7 +217,7 @@ You can [configure](#customizing-the-container-scanning-settings) analyzers by u | `ADDITIONAL_CA_CERT_BUNDLE` | `""` | Bundle of CA certs that you want to trust. See [Using a custom SSL CA certificate authority](#using-a-custom-ssl-ca-certificate-authority) for more details. | All | | `CI_APPLICATION_REPOSITORY` | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | Docker repository URL for the image to be scanned. | All | | `CI_APPLICATION_TAG` | `$CI_COMMIT_SHA` | Docker repository tag for the image to be scanned. | All | -| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:4` | Docker image of the analyzer. | All | +| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:5` | Docker image of the analyzer. | All | | `CS_DEFAULT_BRANCH_IMAGE` | `""` | The name of the `DOCKER_IMAGE` on the default branch. See [Setting the default branch image](#setting-the-default-branch-image) for more details. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/338877) in GitLab 14.5. | All | | `CS_DISABLE_DEPENDENCY_LIST` | `"false"` | Disable Dependency Scanning for packages installed in the scanned image. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345434) in GitLab 14.6. | All | | `CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` | `"true"` | Disable scanning for language-specific packages installed in the scanned image. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345434) in GitLab 14.6. | All | @@ -250,9 +250,9 @@ standard tag plus the `-fips` extension. | Scanner name | `CS_ANALYZER_IMAGE` | | --------------- | ------------------- | -| Default (Trivy) | `registry.gitlab.com/security-products/container-scanning:4-fips` | -| Grype | `registry.gitlab.com/security-products/container-scanning/grype:4-fips` | -| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:4-fips` | +| Default (Trivy) | `registry.gitlab.com/security-products/container-scanning:5-fips` | +| Grype | `registry.gitlab.com/security-products/container-scanning/grype:5-fips` | +| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:5-fips` | NOTE: Prior to GitLab 15.0, the `-ubi` image extension is also available. GitLab 15.0 and later only @@ -305,9 +305,9 @@ The following options are available: | Scanner name | `CS_ANALYZER_IMAGE` | | ------------ | ------------------- | -| Default ([Trivy](https://github.com/aquasecurity/trivy)) | `registry.gitlab.com/security-products/container-scanning:4` | -| [Grype](https://github.com/anchore/grype) | `registry.gitlab.com/security-products/container-scanning/grype:4` | -| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:4` | +| Default ([Trivy](https://github.com/aquasecurity/trivy)) | `registry.gitlab.com/security-products/container-scanning:5` | +| [Grype](https://github.com/anchore/grype) | `registry.gitlab.com/security-products/container-scanning/grype:5` | +| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:5` | If you're migrating from a GitLab 13.x release to a GitLab 14.x release and have customized the `container_scanning` job or its CI variables, you might need to perform these migration steps in @@ -320,7 +320,7 @@ your CI file: - `SECURE_ANALYZERS_PREFIX` 1. Review the `CS_ANALYZER_IMAGE` variable. It no longer depends on the variables above and its new - default value is `registry.gitlab.com/security-products/container-scanning:4`. If you have an + default value is `registry.gitlab.com/security-products/container-scanning:5`. If you have an offline environment, see [Running container scanning in an offline environment](#running-container-scanning-in-an-offline-environment). @@ -532,9 +532,9 @@ For container scanning, import the following images from `registry.gitlab.com` i [local Docker container registry](../../packages/container_registry/index.md): ```plaintext -registry.gitlab.com/security-products/container-scanning:4 -registry.gitlab.com/security-products/container-scanning/grype:4 -registry.gitlab.com/security-products/container-scanning/trivy:4 +registry.gitlab.com/security-products/container-scanning:5 +registry.gitlab.com/security-products/container-scanning/grype:5 +registry.gitlab.com/security-products/container-scanning/trivy:5 ``` The process for importing Docker images into a local offline Docker registry depends on @@ -574,7 +574,7 @@ following `.gitlab-ci.yml` example as a template. ```yaml variables: - SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:4 + SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:5 TARGET_IMAGE: $CI_REGISTRY/namespace/gitlab-container-scanning image: docker:stable @@ -827,6 +827,7 @@ For information on this, see the [general Application Security troubleshooting s as the default for container scanning, and also [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326279) an integration with [Grype](https://github.com/anchore/grype) as an alternative scanner. +- GitLab 15.0 changed the major analyzer version from `4` to `5`. Other changes to the container scanning analyzer can be found in the project's [changelog](https://gitlab.com/gitlab-org/security-products/analyzers/container-scanning/-/blob/master/CHANGELOG.md). diff --git a/doc/user/group/iterations/index.md b/doc/user/group/iterations/index.md index 78330a38bd5..a5944314a50 100644 --- a/doc/user/group/iterations/index.md +++ b/doc/user/group/iterations/index.md @@ -182,7 +182,7 @@ administrator to [enable the feature flag](../../../administration/feature_flags On GitLab.com, this feature is not available. This feature is not ready for production use. Iteration cadences automate iteration scheduling. You can use them to -automate creating iterations every 1, 2, 3, 4, or 6 weeks. You can also +automate creating iterations every 1, 2, 3, or 4 weeks. You can also configure iteration cadences to automatically roll over incomplete issues to the next iteration. ### Create an iteration cadence @@ -198,7 +198,35 @@ To create an iteration cadence: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Issues > Iterations**. 1. Select **New iteration cadence**. -1. Fill out required fields, and select **Create iteration cadence**. The cadence list page opens. +1. Complete the fields. + - Enter the title and description of the iteration cadence. + - Enter the start date of the iteration cadence. + - From the **Duration** dropdown list, select how many weeks each iteration should last. + - From the **Future iterations** dropdown list, select how many future iterations should be + created and maintained by GitLab. + - Optional. To move incomplete issues to the next iteration, select **Roll over issues**. +1. Select **Create cadence**. The cadence list page opens. + +### Edit an iteration cadence + +Prerequisites: + +- You must have at least the Developer role for a group. + +To edit an iteration cadence: + +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Issues > Iterations**. +1. Select **Edit iteration cadence**. + +When you edit the **Duration**, **Future iterations**, or **Start date** fields, +only future iterations are affected. + +You can edit the start date of a cadence if the cadence has not started yet. + +Editing **Future iterations** is a non-destructive action. +If ten future iterations already exist, changing the number under **Future iterations** to `2` +doesn't delete the eight existing future iterations. ### Delete an iteration cadence @@ -217,18 +245,38 @@ To delete an iteration cadence: 1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete. 1. Select **Delete cadence** in the confirmation modal. -### Convert manual cadence to use automatic scheduling +### Manual iteration cadences + +When you **enable** the iteration cadences feature, all previously +created iterations are added to a default iteration cadence. +You can continue to add, edit, and remove iterations in +this default cadence. + +#### Convert a manual cadence to use automatic scheduling WARNING: -The upgrade is irreversible. After it's done, manual iteration cadences cannot be created. +The upgrade is irreversible. After it's done, a new manual iteration cadence cannot be created. -When you **enable** the iteration cadences feature, all iterations are added -to a default iteration cadence. -In this default iteration cadence, you can continue to add, edit, and remove iterations. +Prerequisites: +- You must have created [iterations](#iterations) without cadences before enabling iteration cadences for your group. To upgrade the iteration cadence to use the automation features: 1. On the top bar, select **Menu > Groups** and find your group. 1. On the left sidebar, select **Issues > Iterations**. 1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence you want to upgrade. -1. Fill out required fields, and select **Save changes**. +1. Complete the required fields **Duration** and **Future iterations**. +1. Select **Save changes**. + +#### Start dates of converted cadences + +The start date of your converted cadence is set to the start date of its +**first** existing iteration. + +If you attempt to set a new start date, the conversion fails with an error message. +If your manual cadence is empty, converting it to use automatic scheduling is effectively +the same as creating a new automated cadence. + +During the conversion process GitLab does not delete or modify existing **ongoing** or +**closed** iterations. If you have iterations with start dates in the future, +they are updated to fit your cadence settings. diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb index eb43ed1d37a..7163225777a 100644 --- a/lib/api/admin/instance_clusters.rb +++ b/lib/api/admin/instance_clusters.rb @@ -137,7 +137,7 @@ module API end def ensure_feature_enabled! - not_found! unless Feature.enabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops) + not_found! unless clusterable_instance.certificate_based_clusters_enabled? end end end diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb index b95d17494db..edaa32c26c4 100644 --- a/lib/api/group_clusters.rb +++ b/lib/api/group_clusters.rb @@ -139,7 +139,7 @@ module API end def ensure_feature_enabled! - not_found! unless Feature.enabled?(:certificate_based_clusters, user_group, default_enabled: :yaml, type: :ops) + not_found! unless user_group.certificate_based_clusters_enabled? end end end diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index 00ac255c365..4644d38ea80 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -144,7 +144,9 @@ module API end def ensure_feature_enabled! - not_found! unless Feature.enabled?(:certificate_based_clusters, user_project.namespace, default_enabled: :yaml, type: :ops) + namespace = user_project.namespace + + not_found! unless namespace.certificate_based_clusters_enabled? end end end diff --git a/lib/gitlab/background_migration/backfill_integrations_type_new.rb b/lib/gitlab/background_migration/backfill_integrations_type_new.rb index a234cebfce5..6f33472af7d 100644 --- a/lib/gitlab/background_migration/backfill_integrations_type_new.rb +++ b/lib/gitlab/background_migration/backfill_integrations_type_new.rb @@ -22,7 +22,7 @@ module Gitlab private def connection - ActiveRecord::Base.connection + ApplicationRecord.connection end def process_sub_batch(sub_batch) diff --git a/lib/gitlab/background_migration/backfill_issue_search_data.rb b/lib/gitlab/background_migration/backfill_issue_search_data.rb index ec206cbfd41..e408fd0cda6 100644 --- a/lib/gitlab/background_migration/backfill_issue_search_data.rb +++ b/lib/gitlab/background_migration/backfill_issue_search_data.rb @@ -9,7 +9,7 @@ module Gitlab include Gitlab::Database::DynamicModelHelpers def perform(start_id, stop_id, batch_table, batch_column, sub_batch_size, pause_ms) - define_batchable_model(batch_table, connection: ActiveRecord::Base.connection).where(batch_column => start_id..stop_id).each_batch(of: sub_batch_size) do |sub_batch| + define_batchable_model(batch_table, connection: ApplicationRecord.connection).where(batch_column => start_id..stop_id).each_batch(of: sub_batch_size) do |sub_batch| update_search_data(sub_batch) sleep(pause_ms * 0.001) diff --git a/lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb b/lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb index 1ed147d67c7..5f3d830c48d 100644 --- a/lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb +++ b/lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb @@ -26,7 +26,7 @@ module Gitlab private def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id) - define_batchable_model(source_table, connection: ActiveRecord::Base.connection) + define_batchable_model(source_table, connection: ApplicationRecord.connection) .joins('INNER JOIN namespaces ON members.source_id = namespaces.id') .where(source_key_column => start_id..stop_id) .where(type: 'GroupMember') diff --git a/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb b/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb index fe3edd3322b..0585924cb7b 100644 --- a/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb +++ b/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb @@ -27,7 +27,7 @@ module Gitlab private def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id) - define_batchable_model(source_table, connection: ActiveRecord::Base.connection) + define_batchable_model(source_table, connection: ApplicationRecord.connection) .joins('inner join namespaces on routes.source_id = namespaces.id') .where(source_key_column => start_id..stop_id) .where(namespace_id: nil) diff --git a/lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb b/lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb index 1f0d606f001..7f75b64e98a 100644 --- a/lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb +++ b/lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb @@ -13,7 +13,7 @@ module Gitlab cleanup_gin_index('routes') batch_metrics.time_operation(:update_all) do - ActiveRecord::Base.connection.execute <<~SQL + ApplicationRecord.connection.execute <<~SQL WITH route_and_ns(route_id, project_namespace_id) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} ( #{sub_batch.to_sql} ) @@ -37,15 +37,15 @@ module Gitlab def cleanup_gin_index(table_name) sql = "select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'" - index_names = ActiveRecord::Base.connection.select_values(sql) + index_names = ApplicationRecord.connection.select_values(sql) index_names.each do |index_name| - ActiveRecord::Base.connection.execute("select gin_clean_pending_list('#{index_name}')") + ApplicationRecord.connection.execute("select gin_clean_pending_list('#{index_name}')") end end def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id) - define_batchable_model(source_table, connection: ActiveRecord::Base.connection) + define_batchable_model(source_table, connection: ApplicationRecord.connection) .joins('INNER JOIN projects ON routes.source_id = projects.id') .where(source_key_column => start_id..stop_id) .where(namespace_id: nil) diff --git a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb index 79e7a2f2279..587de1bcb5a 100644 --- a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb +++ b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb @@ -29,7 +29,7 @@ module Gitlab WHERE namespaces.id = calculated_ids.id AND namespaces.traversal_ids = '{}' SQL - ActiveRecord::Base.connection.execute(update_sql) + ApplicationRecord.connection.execute(update_sql) sleep PAUSE_SECONDS end diff --git a/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb b/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb index 170af90805a..3bf6bf993dd 100644 --- a/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb +++ b/lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb @@ -16,7 +16,7 @@ module Gitlab private def execute(sql) - @connection ||= ::ActiveRecord::Base.connection + @connection ||= ApplicationRecord.connection @connection.execute(sql) end diff --git a/lib/gitlab/background_migration/backfill_user_namespace.rb b/lib/gitlab/background_migration/backfill_user_namespace.rb index ab569e236fb..df6b1f083c3 100644 --- a/lib/gitlab/background_migration/backfill_user_namespace.rb +++ b/lib/gitlab/background_migration/backfill_user_namespace.rb @@ -25,7 +25,7 @@ module Gitlab private def connection - ActiveRecord::Base.connection + ApplicationRecord.connection end def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id) diff --git a/lib/gitlab/background_migration/delete_orphaned_deployments.rb b/lib/gitlab/background_migration/delete_orphaned_deployments.rb index 5d41a46c8cd..4a3a12ab53d 100644 --- a/lib/gitlab/background_migration/delete_orphaned_deployments.rb +++ b/lib/gitlab/background_migration/delete_orphaned_deployments.rb @@ -15,7 +15,7 @@ module Gitlab end def orphaned_deployments - define_batchable_model('deployments', connection: ActiveRecord::Base.connection) + define_batchable_model('deployments', connection: ApplicationRecord.connection) .where('NOT EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)') end diff --git a/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb b/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb index 9a88eb8ea06..dad5da875ab 100644 --- a/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb +++ b/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb @@ -32,7 +32,7 @@ module Gitlab private def execute(sql) - ActiveRecord::Base + ApplicationRecord .connection .execute(sql) end diff --git a/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb b/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb index defd9ea832b..3772430d0b7 100644 --- a/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb +++ b/lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb @@ -21,7 +21,7 @@ module Gitlab backfill_project_namespaces_service.cleanup_gin_index('projects') project_ids.each_slice(SUB_BATCH_SIZE) do |ids| - ActiveRecord::Base.connection.execute(update_projects_name_and_path_sql(ids)) + ApplicationRecord.connection.execute(update_projects_name_and_path_sql(ids)) end backfill_project_namespaces_service.backfill_project_namespaces diff --git a/lib/gitlab/background_migration/fix_projects_without_project_feature.rb b/lib/gitlab/background_migration/fix_projects_without_project_feature.rb index 83c01afa432..c21f9c1d50f 100644 --- a/lib/gitlab/background_migration/fix_projects_without_project_feature.rb +++ b/lib/gitlab/background_migration/fix_projects_without_project_feature.rb @@ -14,7 +14,7 @@ module Gitlab private def create_missing!(from_id, to_id) - result = ActiveRecord::Base.connection.select_one(sql(from_id, to_id)) + result = ApplicationRecord.connection.select_one(sql(from_id, to_id)) return 0 unless result result['number_of_created_records'] diff --git a/lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb b/lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb index b8e4562b3bf..496ec0bd0a1 100644 --- a/lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb +++ b/lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb @@ -120,14 +120,14 @@ module Gitlab end def create_missing(from_id, to_id) - result = ActiveRecord::Base.connection.select_one(create_sql(from_id, to_id)) + result = ApplicationRecord.connection.select_one(create_sql(from_id, to_id)) return unless result logger.info(message: "#{self.class}: created missing services for #{result['number_of_created_records']} projects in id=#{from_id}...#{to_id}") end def update_inconsistent(from_id, to_id) - result = ActiveRecord::Base.connection.select_one(update_sql(from_id, to_id)) + result = ApplicationRecord.connection.select_one(update_sql(from_id, to_id)) return unless result logger.info(message: "#{self.class}: updated inconsistent services for #{result['number_of_updated_records']} projects in id=#{from_id}...#{to_id}") diff --git a/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb b/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb index ec4631d1e34..d7d24960a41 100644 --- a/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb +++ b/lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb @@ -7,7 +7,7 @@ module Gitlab include Gitlab::Database::DynamicModelHelpers def perform(start_id, end_id) - define_batchable_model('integrations', connection: ::ActiveRecord::Base.connection) + define_batchable_model('integrations', connection: ApplicationRecord.connection) .where(id: start_id..end_id, type_new: %w[Integrations::Confluence Integrations::Shimo]) .update_all(category: 'third_party_wiki') diff --git a/lib/gitlab/background_migration/migrate_stage_status.rb b/lib/gitlab/background_migration/migrate_stage_status.rb deleted file mode 100644 index 6a29a632577..00000000000 --- a/lib/gitlab/background_migration/migrate_stage_status.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true -# rubocop:disable Metrics/AbcSize -# rubocop:disable Style/Documentation - -module Gitlab - module BackgroundMigration - class MigrateStageStatus - STATUSES = { created: 0, pending: 1, running: 2, success: 3, - failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze - - class Build < ActiveRecord::Base - self.table_name = 'ci_builds' - - scope :latest, -> { where(retried: [false, nil]) } - scope :created, -> { where(status: 'created') } - scope :running, -> { where(status: 'running') } - scope :pending, -> { where(status: 'pending') } - scope :success, -> { where(status: 'success') } - scope :failed, -> { where(status: 'failed') } - scope :canceled, -> { where(status: 'canceled') } - scope :skipped, -> { where(status: 'skipped') } - scope :manual, -> { where(status: 'manual') } - - scope :failed_but_allowed, -> do - where(allow_failure: true, status: [:failed, :canceled]) - end - - scope :exclude_ignored, -> do - where("allow_failure = ? OR status IN (?)", - false, %w[created pending running success skipped]) - end - - def self.status_sql - scope_relevant = latest.exclude_ignored - scope_warnings = latest.failed_but_allowed - - builds = scope_relevant.select('count(*)').to_sql - created = scope_relevant.created.select('count(*)').to_sql - success = scope_relevant.success.select('count(*)').to_sql - manual = scope_relevant.manual.select('count(*)').to_sql - pending = scope_relevant.pending.select('count(*)').to_sql - running = scope_relevant.running.select('count(*)').to_sql - skipped = scope_relevant.skipped.select('count(*)').to_sql - canceled = scope_relevant.canceled.select('count(*)').to_sql - warnings = scope_warnings.select('count(*) > 0').to_sql - - <<-SQL.strip_heredoc - (CASE - WHEN (#{builds}) = (#{skipped}) AND (#{warnings}) THEN #{STATUSES[:success]} - WHEN (#{builds}) = (#{skipped}) THEN #{STATUSES[:skipped]} - WHEN (#{builds}) = (#{success}) THEN #{STATUSES[:success]} - WHEN (#{builds}) = (#{created}) THEN #{STATUSES[:created]} - WHEN (#{builds}) = (#{success}) + (#{skipped}) THEN #{STATUSES[:success]} - WHEN (#{builds}) = (#{success}) + (#{skipped}) + (#{canceled}) THEN #{STATUSES[:canceled]} - WHEN (#{builds}) = (#{created}) + (#{skipped}) + (#{pending}) THEN #{STATUSES[:pending]} - WHEN (#{running}) + (#{pending}) > 0 THEN #{STATUSES[:running]} - WHEN (#{manual}) > 0 THEN #{STATUSES[:manual]} - WHEN (#{created}) > 0 THEN #{STATUSES[:running]} - ELSE #{STATUSES[:failed]} - END) - SQL - end - end - - def perform(start_id, stop_id) - status_sql = Build - .where('ci_builds.commit_id = ci_stages.pipeline_id') - .where('ci_builds.stage = ci_stages.name') - .status_sql - - sql = <<-SQL - UPDATE ci_stages SET status = (#{status_sql}) - WHERE ci_stages.status IS NULL - AND ci_stages.id BETWEEN #{start_id.to_i} AND #{stop_id.to_i} - SQL - - ActiveRecord::Base.connection.execute(sql) - end - end - end -end diff --git a/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb b/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb index c01545e5dca..06422ed282f 100644 --- a/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb +++ b/lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb @@ -22,7 +22,7 @@ module Gitlab private def process_batch(from_id, to_id) - ActiveRecord::Base.connection.execute(update_sql(from_id, to_id)) + ApplicationRecord.connection.execute(update_sql(from_id, to_id)) logger.info(message: "#{self.class}: Copied container_registry_enabled values for projects with IDs between #{from_id}..#{to_id}") end diff --git a/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb b/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb index 9e102ea1517..a9611e9814c 100644 --- a/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb +++ b/lib/gitlab/background_migration/populate_container_repository_migration_plan.rb @@ -33,7 +33,7 @@ module Gitlab private def connection - @connection ||= ::ActiveRecord::Base.connection + @connection ||= ApplicationRecord.connection end def execute(sql) diff --git a/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb b/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb index 769ca4be7f3..1f2b55004e4 100644 --- a/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb +++ b/lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb @@ -15,7 +15,7 @@ module Gitlab def perform(start_id, stop_id) Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch| - ActiveRecord::Base.connection.execute(<<~SQL) + ApplicationRecord.connection.execute(<<~SQL) WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql}) UPDATE topics SET non_private_projects_count = ( diff --git a/lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb b/lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb index 1d96872d445..2495cb51364 100644 --- a/lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb +++ b/lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb @@ -15,7 +15,7 @@ module Gitlab def perform(start_id, stop_id) Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch| - ActiveRecord::Base.connection.execute(<<~SQL) + ApplicationRecord.connection.execute(<<~SQL) WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql}) UPDATE topics SET total_projects_count = (SELECT COUNT(*) FROM project_topics WHERE topic_id = batched_relation.id) diff --git a/lib/gitlab/background_migration/populate_vulnerability_reads.rb b/lib/gitlab/background_migration/populate_vulnerability_reads.rb index 7b6d4c1ff81..5e6475a3d1a 100644 --- a/lib/gitlab/background_migration/populate_vulnerability_reads.rb +++ b/lib/gitlab/background_migration/populate_vulnerability_reads.rb @@ -26,7 +26,7 @@ module Gitlab end def connection - ActiveRecord::Base.connection + ApplicationRecord.connection end def insert_query(start_id, end_id) diff --git a/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb b/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb index bd7d7d02162..91a0db08d5b 100644 --- a/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb +++ b/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb @@ -55,10 +55,10 @@ module Gitlab end def cleanup_gin_index(table_name) - index_names = ActiveRecord::Base.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'") + index_names = ApplicationRecord.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'") index_names.each do |index_name| - ActiveRecord::Base.connection.execute("select gin_clean_pending_list('#{index_name}')") + ApplicationRecord.connection.execute("select gin_clean_pending_list('#{index_name}')") end end @@ -77,7 +77,7 @@ module Gitlab projects = IsolatedModels::Project.where(id: project_ids) .select("projects.id, projects.name, projects.path, projects.namespace_id, projects.visibility_level, shared_runners_enabled, '#{PROJECT_NAMESPACE_STI_NAME}', now(), now()") - ActiveRecord::Base.connection.execute <<~SQL + ApplicationRecord.connection.execute <<~SQL INSERT INTO namespaces (tmp_project_id, name, path, parent_id, visibility_level, shared_runners_enabled, type, created_at, updated_at) #{projects.to_sql} ON CONFLICT DO NOTHING; @@ -89,7 +89,7 @@ module Gitlab .joins("INNER JOIN namespaces ON projects.id = namespaces.tmp_project_id") .select("namespaces.id, namespaces.tmp_project_id") - ActiveRecord::Base.connection.execute <<~SQL + ApplicationRecord.connection.execute <<~SQL WITH cte(project_namespace_id, project_id) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} ( #{projects.to_sql} ) @@ -105,7 +105,7 @@ module Gitlab .joins("INNER JOIN namespaces n2 ON namespaces.parent_id = n2.id") .select("namespaces.id as project_namespace_id, n2.traversal_ids") - ActiveRecord::Base.connection.execute <<~SQL + ApplicationRecord.connection.execute <<~SQL UPDATE namespaces SET traversal_ids = array_append(project_namespaces.traversal_ids, project_namespaces.project_namespace_id) FROM (#{namespaces.to_sql}) as project_namespaces(project_namespace_id, traversal_ids) diff --git a/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb b/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb index 323f109449b..4acef9029f9 100644 --- a/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb +++ b/lib/gitlab/background_migration/remove_vulnerability_finding_links.rb @@ -10,7 +10,7 @@ module Gitlab include Gitlab::Database::DynamicModelHelpers def perform(start_id, stop_id) - define_batchable_model('vulnerability_finding_links', connection: ActiveRecord::Base.connection) + define_batchable_model('vulnerability_finding_links', connection: ApplicationRecord.connection) .where(id: start_id..stop_id) .delete_all end diff --git a/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb b/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb index f54bb8256d0..38932e52bb0 100644 --- a/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb +++ b/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb @@ -28,7 +28,7 @@ module Gitlab end def connection - @connection ||= ::ActiveRecord::Base.connection + @connection ||= ApplicationRecord.connection end def execute(sql) diff --git a/lib/gitlab/background_migration/update_timelogs_project_id.rb b/lib/gitlab/background_migration/update_timelogs_project_id.rb index 24c9967b88e..69bb5cf6e6d 100644 --- a/lib/gitlab/background_migration/update_timelogs_project_id.rb +++ b/lib/gitlab/background_migration/update_timelogs_project_id.rb @@ -36,7 +36,7 @@ module Gitlab end def execute(sql) - @connection ||= ::ActiveRecord::Base.connection + @connection ||= ApplicationRecord.connection @connection.execute(sql) end end diff --git a/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb b/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb index f5ba9e63333..10db9f5064a 100644 --- a/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb +++ b/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb @@ -5,7 +5,7 @@ module Gitlab module BackgroundMigration class UpdateUsersWhereTwoFactorAuthRequiredFromGroup # rubocop:disable Metrics/ClassLength def perform(start_id, stop_id) - ActiveRecord::Base.connection.execute <<~SQL + ApplicationRecord.connection.execute <<~SQL UPDATE users SET diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index f8fce1abc06..606814c72ad 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -15,7 +15,7 @@ module Gitlab ALLOWED_KEYS = %i[junit codequality sast secret_detection dependency_scanning container_scanning dast performance browser_performance load_performance license_scanning metrics lsif - dotenv cobertura terraform accessibility cluster_applications + dotenv cobertura terraform accessibility requirements coverage_fuzzing api_fuzzing cluster_image_scanning coverage_report].freeze @@ -48,7 +48,6 @@ module Gitlab validates :cobertura, array_of_strings_or_string: true validates :terraform, array_of_strings_or_string: true validates :accessibility, array_of_strings_or_string: true - validates :cluster_applications, array_of_strings_or_string: true # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/333441 validates :requirements, array_of_strings_or_string: true end diff --git a/lib/gitlab/graphql/find_argument_in_parent.rb b/lib/gitlab/graphql/find_argument_in_parent.rb deleted file mode 100644 index 1f83f8fce7a..00000000000 --- a/lib/gitlab/graphql/find_argument_in_parent.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - module FindArgumentInParent - # Searches up the GraphQL AST and returns the first matching argument - # passed to a node - def self.find(parent, argument, limit_depth: nil) - argument = argument.to_s.camelize(:lower).to_sym - depth = 0 - - while parent.respond_to?(:parent) - args = node_args(parent) - return args[argument] if args.key?(argument) - - depth += 1 - return if limit_depth && depth >= limit_depth - - parent = parent.parent - end - end - - class << self - private - - def node_args(node) - node.irep_node.arguments - end - end - end - end -end diff --git a/lib/sidebars/groups/menus/kubernetes_menu.rb b/lib/sidebars/groups/menus/kubernetes_menu.rb index e3b6104e517..0d845978a93 100644 --- a/lib/sidebars/groups/menus/kubernetes_menu.rb +++ b/lib/sidebars/groups/menus/kubernetes_menu.rb @@ -22,7 +22,8 @@ module Sidebars override :render? def render? clusterable = context.group - Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) && + + clusterable.certificate_based_clusters_enabled? && can?(context.current_user, :read_cluster, clusterable) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 923d72b9f20..5311e9edbdc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -43374,6 +43374,12 @@ msgstr "" msgid "You cannot approve your own deployment." msgstr "" +msgid "You cannot change the start date after the cadence has started. Please create a new cadence." +msgstr "" + +msgid "You cannot change the start date because the first iteration has already started on %{start_date}." +msgstr "" + msgid "You cannot combine replace_ids with add_ids or remove_ids" msgstr "" diff --git a/qa/qa/page/component/new_snippet.rb b/qa/qa/page/component/new_snippet.rb index 6ccf8a4043e..9c4408e36e4 100644 --- a/qa/qa/page/component/new_snippet.rb +++ b/qa/qa/page/component/new_snippet.rb @@ -76,8 +76,7 @@ module QA end def click_create_snippet_button - wait_until(reload: false) { !find_element(:submit_button).disabled? } - click_element(:submit_button) + click_element_coordinates(:submit_button) wait_until(reload: false) do has_no_element?(:snippet_title_field) end diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb index d84a053591c..8af3eb5693c 100644 --- a/qa/qa/page/dashboard/snippet/edit.rb +++ b/qa/qa/page/dashboard/snippet/edit.rb @@ -63,8 +63,7 @@ module QA end def save_changes - wait_until(reload: false) { !find_element(:submit_button).disabled? } - click_element(:submit_button) + click_element_coordinates(:submit_button) wait_until(reload: false) do has_no_element?(:file_name_field) end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb index 38adf2f5d55..b20d0929e9c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', :reliable do context 'Add batch suggestions to a Merge Request' do let(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index f8129c9ccba..91b0940f137 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', :reliable do describe 'Protected branch support' do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb index 0b63d9a1edb..a50b995e483 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', :reliable do describe 'Multiple file snippet' do let(:first_file_content) { 'First file content' } let(:second_file_content) { 'Second file content' } diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb index b45624381c8..4b8edf8b792 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', :reliable do context 'Content Editor' do let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! } let(:page_title) { 'Content Editor Page' } diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js index a983ec43557..d7d46d0d415 100644 --- a/spec/frontend/security_configuration/components/app_spec.js +++ b/spec/frontend/security_configuration/components/app_spec.js @@ -54,13 +54,22 @@ describe('App component', () => { const createComponent = ({ shouldShowCallout = true, - license = LICENSE_ULTIMATE, + licenseQueryResponse = LICENSE_ULTIMATE, ...propsData }) => { userCalloutDismissSpy = jest.fn(); mockApollo = createMockApollo([ - [currentLicenseQuery, jest.fn().mockResolvedValue(getCurrentLicensePlanResponse(license))], + [ + currentLicenseQuery, + jest + .fn() + .mockResolvedValue( + licenseQueryResponse instanceof Error + ? licenseQueryResponse + : getCurrentLicensePlanResponse(licenseQueryResponse), + ), + ], ]); wrapper = extendedWrapper( @@ -484,19 +493,23 @@ describe('App component', () => { }); it.each` - license | display - ${LICENSE_ULTIMATE} | ${true} - ${LICENSE_PREMIUM} | ${false} - ${LICENSE_FREE} | ${false} - ${null} | ${false} - `('displays $display for license $license', async ({ license, display }) => { - createComponent({ - license, - augmentedSecurityFeatures: securityFeaturesMock, - augmentedComplianceFeatures: complianceFeaturesMock, - }); - await waitForPromises(); - expect(findVulnerabilityManagementTab().exists()).toBe(display); - }); + licenseQueryResponse | display + ${LICENSE_ULTIMATE} | ${true} + ${LICENSE_PREMIUM} | ${false} + ${LICENSE_FREE} | ${false} + ${null} | ${true} + ${new Error()} | ${true} + `( + 'displays $display for license $licenseQueryResponse', + async ({ licenseQueryResponse, display }) => { + createComponent({ + licenseQueryResponse, + augmentedSecurityFeatures: securityFeaturesMock, + augmentedComplianceFeatures: complianceFeaturesMock, + }); + await waitForPromises(); + expect(findVulnerabilityManagementTab().exists()).toBe(display); + }, + ); }); }); diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb index d98138f6385..8eab0222cf6 100644 --- a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb @@ -18,8 +18,7 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do let(:project) { issue.project } let(:params) { {} } let(:current_user) { authorized_user } - let(:parent_args) { { irrelevant: 1.2 } } - let(:parent) { double('Parent', parent: nil, irep_node: double(arguments: parent_args)) } + let(:query_context) { { current_user: current_user } } before do enable_design_management @@ -107,7 +106,9 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do end context 'by at_version in parent' do - let(:parent_args) { { atVersion: global_id_of(first_version) } } + before do + query_context[:at_version_argument] = first_version.to_global_id + end it_behaves_like 'a query for all_versions up to the first_version' end @@ -126,8 +127,8 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do it_behaves_like 'a source of versions' end - def resolve_versions(obj, context = { current_user: current_user }) - eager_resolve(resolver, obj: obj, parent: parent, args: params, ctx: context) + def resolve_versions(obj) + eager_resolve(resolver, obj: obj, args: params, ctx: query_context) end end end diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index c93762416f5..0304aac18ae 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -295,7 +295,7 @@ RSpec.describe ApplicationSettingsHelper do it { is_expected.to eq([%w(Track track), %w(Compress compress)]) } end - describe '#instance_clusters_enabled?' do + describe '#instance_clusters_enabled?', :request_store do let_it_be(:user) { create(:user) } subject { helper.instance_clusters_enabled? } diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 061d8f34c8d..ff3ec6196f0 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -48,7 +48,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do :cobertura | 'cobertura-coverage.xml' :terraform | 'tfplan.json' :accessibility | 'gl-accessibility.json' - :cluster_applications | 'gl-cluster-applications.json' end with_them do diff --git a/spec/lib/gitlab/graphql/find_argument_in_parent_spec.rb b/spec/lib/gitlab/graphql/find_argument_in_parent_spec.rb deleted file mode 100644 index 1b9301cd1aa..00000000000 --- a/spec/lib/gitlab/graphql/find_argument_in_parent_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::FindArgumentInParent do - describe '#find' do - def build_node(parent = nil, args: {}) - props = { irep_node: double(arguments: args) } - props[:parent] = parent if parent # The root node shouldn't respond to parent - - double(props) - end - - let(:parent) do - build_node( - build_node( - build_node( - build_node, - args: { myArg: 1 } - ) - ) - ) - end - - let(:arg_name) { :my_arg } - - it 'searches parents and returns the argument' do - expect(described_class.find(parent, :my_arg)).to eq(1) - end - - it 'can find argument when passed in as both Ruby and GraphQL-formatted symbols and strings' do - [:my_arg, :myArg, 'my_arg', 'myArg'].each do |arg| - expect(described_class.find(parent, arg)).to eq(1) - end - end - - it 'returns nil if no arguments found in parents' do - expect(described_class.find(parent, :bar)).to eq(nil) - end - - it 'can limit the depth it searches to' do - expect(described_class.find(parent, :my_arg, limit_depth: 1)).to eq(nil) - end - end -end diff --git a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb index 36d5b3376b7..5bf8be9d6e5 100644 --- a/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do +RSpec.describe Sidebars::Groups::Menus::KubernetesMenu, :request_store do let_it_be(:owner) { create(:user) } let_it_be(:group) do build(:group, :private).tap do |g| diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e03996e96ff..f95feee47a9 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1446,6 +1446,44 @@ RSpec.describe Ci::Build do expect(deployment).to be_canceled end end + + # Mimic playing a manual job that needs another job. + # `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502 + context 'when transits from skipped to created to running' do + before do + build.skip! + end + + context 'during skipped to created' do + let(:event) { :process! } + + it 'transitions to created' do + subject + + expect(deployment).to be_created + end + end + + context 'during created to running' do + let(:event) { :run! } + + before do + build.process! + build.enqueue! + end + + it 'transitions to running and calls webhook' do + freeze_time do + expect(Deployments::HooksWorker) + .to receive(:perform_async).with(deployment_id: deployment.id, status_changed_at: Time.current) + + subject + end + + expect(deployment).to be_running + end + end + end end describe '#on_stop' do diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 705b9b4cc65..409353bdbcf 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -858,12 +858,24 @@ RSpec.describe Deployment do end end - it 'tracks an exception if an invalid status transition is detected' do - expect(Gitlab::ErrorTracking) + context 'tracks an exception if an invalid status transition is detected' do + it do + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with(instance_of(described_class::StatusUpdateError), deployment_id: deploy.id) + + expect(deploy.update_status('running')).to eq(false) + end + + it do + deploy.update_status('success') + + expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with(instance_of(described_class::StatusUpdateError), deployment_id: deploy.id) - expect(deploy.update_status('running')).to eq(false) + expect(deploy.update_status('created')).to eq(false) + end end it 'tracks an exception if an invalid argument' do @@ -871,7 +883,7 @@ RSpec.describe Deployment do .to receive(:track_exception) .with(instance_of(described_class::StatusUpdateError), deployment_id: deploy.id) - expect(deploy.update_status('created')).to eq(false) + expect(deploy.update_status('recreate')).to eq(false) end context 'mapping status to event' do @@ -893,6 +905,16 @@ RSpec.describe Deployment do deploy.update_status(status) end end + + context 'for created status update' do + let(:deploy) { create(:deployment, status: :created) } + + it 'calls the correct method' do + expect(deploy).to receive(:create!) + + deploy.update_status('created') + end + end end end @@ -974,7 +996,9 @@ RSpec.describe Deployment do context 'with created build' do let(:build_status) { :created } - it_behaves_like 'ignoring build' + it_behaves_like 'gracefully handling error' do + let(:error_message) { %Q{Status cannot transition via \"create\"} } + end end context 'with running build' do @@ -1002,7 +1026,9 @@ RSpec.describe Deployment do context 'with created build' do let(:build_status) { :created } - it_behaves_like 'ignoring build' + it_behaves_like 'gracefully handling error' do + let(:error_message) { %Q{Status cannot transition via \"create\"} } + end end context 'with running build' do diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb index a2b914c42f2..7b524be0c43 100644 --- a/spec/models/integration_spec.rb +++ b/spec/models/integration_spec.rb @@ -782,8 +782,16 @@ RSpec.describe Integration do end end - describe '#api_field_names' do - shared_examples 'api field names' do + describe 'field definitions' do + shared_examples '#fields' do + it 'does not return the same array' do + integration = fake_integration.new + + expect(integration.fields).not_to be(integration.fields) + end + end + + shared_examples '#api_field_names' do it 'filters out secret fields' do safe_fields = %w[some_safe_field safe_field url trojan_gift] @@ -816,7 +824,8 @@ RSpec.describe Integration do end end - it_behaves_like 'api field names' + it_behaves_like '#fields' + it_behaves_like '#api_field_names' end context 'when the class uses the field DSL' do @@ -839,7 +848,8 @@ RSpec.describe Integration do end end - it_behaves_like 'api field names' + it_behaves_like '#fields' + it_behaves_like '#api_field_names' end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 09ac15429a5..b70d90d861b 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -2236,4 +2236,40 @@ RSpec.describe Namespace do it_behaves_like 'blocks unsafe serialization' end + + describe '#certificate_based_clusters_enabled?' do + it 'does not call Feature.enabled? twice with request_store', :request_store do + expect(Feature).to receive(:enabled?).once + + namespace.certificate_based_clusters_enabled? + namespace.certificate_based_clusters_enabled? + end + + it 'call Feature.enabled? twice without request_store' do + expect(Feature).to receive(:enabled?).twice + + namespace.certificate_based_clusters_enabled? + namespace.certificate_based_clusters_enabled? + end + + context 'with ff disabled' do + before do + stub_feature_flags(certificate_based_clusters: false) + end + + it 'is truthy' do + expect(namespace.certificate_based_clusters_enabled?).to be_falsy + end + end + + context 'with ff enabled' do + before do + stub_feature_flags(certificate_based_clusters: true) + end + + it 'is truthy' do + expect(namespace.certificate_based_clusters_enabled?).to be_truthy + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index dc0fa6eff66..42fe9f03141 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -833,6 +833,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) } it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) } + it { is_expected.to delegate_method(:certificate_based_clusters_enabled?).to(:namespace).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) } it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) } diff --git a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb index c698e06c2a2..fbec6f98e76 100644 --- a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb +++ b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb @@ -43,5 +43,9 @@ RSpec.shared_context Integrations::EnableSslVerification do expect(names.index('enable_ssl_verification')).to eq insert_index end + + it 'does not insert the field repeatedly' do + expect(integration.fields.pluck(:name)).to eq(integration.fields.pluck(:name)) + end end end diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb index 8c9d1b32671..428e9cc8490 100644 --- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb @@ -101,7 +101,7 @@ RSpec.describe 'layouts/nav/sidebar/_group' do end end - describe 'Kubernetes menu' do + describe 'Kubernetes menu', :request_store do it 'has a link to the group cluster list path' do render |