From 5cbf24858edb03505b16474e3b7b41a49b677ff6 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 17 Oct 2023 18:10:11 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/layout/argument_alignment.yml | 12 - .../layout/empty_line_after_magic_comment.yml | 15 -- ...r_relations_organization.mutation.graphql} | 2 +- .../components/organization_form_wrapper.vue | 4 +- .../graphql/resolvers/kubernetes.js | 2 +- .../components/group_dropdown.vue | 72 ------ .../components/import_target_dropdown.vue | 54 ++++- .../components/import_target_cell.vue | 56 ++--- .../javascripts/integrations/constants.js | 8 + .../index/components/ml_models_index.vue | 11 +- .../models/index/components/model_row.vue | 35 +++ .../routes/models/index/translations.js | 14 +- .../components/email_participants_warning.vue | 7 +- .../javascripts/organizations/mock_data.js | 8 + .../organizations/new/components/app.vue | 73 ++++++ .../create_organization.mutation.graphql | 9 + .../new/graphql/typedefs.graphql | 5 + .../javascripts/organizations/new/index.js | 35 +++ .../shared/components/new_edit_form.vue | 125 ++++++++++ .../organizations/shared/graphql/resolvers.js | 15 +- .../organizations/organizations/new/index.js | 3 + .../components/incubation/pagination.vue | 1 + .../page_bundles/organizations.scss | 4 + .../projects/ml/models_index_component.rb | 1 + .../concerns/packages/finder_helper.rb | 4 +- .../packages/npm/packages_for_user_finder.rb | 7 + app/finders/projects/ml/model_finder.rb | 1 + app/helpers/integrations_helper.rb | 8 - .../organizations/organization_helper.rb | 7 + app/models/ci/catalog/components_project.rb | 94 ++++++++ app/models/integrations/pushover.rb | 4 + app/models/ml/model.rb | 5 + app/models/project.rb | 1 + app/services/ci/components/fetch_service.rb | 15 +- .../find_or_create_model_version_service.rb | 1 - .../organizations/organizations/new.html.haml | 3 + .../add_todo_when_build_fails_worker.rb | 1 + app/workers/concerns/auto_devops_queue.rb | 2 +- app/workers/concerns/chaos_queue.rb | 2 +- .../concerns/limited_capacity/job_tracker.rb | 1 + .../ci_database_worker.rb | 1 + .../github_gists_import/import_gist_worker.rb | 6 +- .../start_import_worker.rb | 2 +- .../stuck_project_import_jobs_worker.rb | 1 + app/workers/projects/after_import_worker.rb | 2 +- .../feature_flags/development/print_wiki.yml | 2 +- .../ops/global_search_epics_tab.yml | 8 + .../audit_event_streaming/graphql_api.md | 160 ++++++++++++- .../settings/user_and_ip_rate_limits.md | 14 ++ doc/api/settings.md | 7 +- .../blueprints/bundle_uri/index.md | 216 ++++++++++++++++++ .../documentation/styleguide/word_list.md | 22 +- doc/security/rate_limits.md | 2 +- .../repository/code_suggestions/index.md | 106 ++++----- .../repository/code_suggestions/saas.md | 17 +- .../code_suggestions/self_managed.md | 17 +- doc/user/search/index.md | 1 + lib/gitlab/ci/components/instance_path.rb | 61 ++--- .../ci/config/external/file/component.rb | 18 +- lib/gitlab/ci/config/yaml/loader.rb | 8 +- lib/gitlab/ci/config/yaml/result.rb | 4 + .../ci/parsers/sbom/cyclonedx_properties.rb | 7 +- .../ci/parsers/sbom/source/base_source.rb | 46 ---- .../parsers/sbom/source/container_scanning.rb | 26 --- .../sbom/source/dependency_scanning.rb | 27 ++- lib/gitlab/github_import/bulk_importing.rb | 12 +- lib/gitlab/github_import/client.rb | 4 +- .../importer/attachments/issues_importer.rb | 2 +- .../attachments/merge_requests_importer.rb | 2 +- .../importer/attachments/notes_importer.rb | 2 +- .../importer/attachments/releases_importer.rb | 2 +- .../importer/diff_note_importer.rb | 2 +- .../importer/pull_requests_importer.rb | 2 +- .../github_import/parallel_scheduling.rb | 6 + lib/gitlab/github_import/user_finder.rb | 3 +- locale/gitlab.pot | 59 +++-- qa/qa/page/group/bulk_import.rb | 12 +- qa/qa/page/project/import/github.rb | 4 +- .../ml/models_index_component_spec.rb | 8 +- .../oauth/tokens_controller_spec.rb | 8 +- .../concerns/packages/finder_helper_spec.rb | 30 ++- .../npm/packages_for_user_finder_spec.rb | 18 ++ spec/finders/projects/ml/model_finder_spec.rb | 4 + spec/frontend/crm/crm_form_spec.js | 2 +- .../crm/organization_form_wrapper_spec.js | 2 +- .../graphql/resolvers/kubernetes_spec.js | 2 +- spec/frontend/import/details/mock_data.js | 6 +- .../components/group_dropdown_spec.js | 94 -------- .../components/import_target_dropdown_spec.js | 55 ++++- .../components/import_table_spec.js | 2 +- .../components/import_target_cell_spec.js | 52 +---- .../index/components/ml_models_index_spec.js | 13 +- .../models/index/components/mock_data.js | 10 +- .../models/index/components/model_row_spec.js | 42 ++++ .../email_participants_warning_spec.js | 6 +- .../organizations/new/components/app_spec.js | 103 +++++++++ .../shared/components/new_edit_form_spec.js | 112 +++++++++ .../organizations/organization_helper_spec.rb | 15 ++ .../ci/components/instance_path_spec.rb | 45 ++-- .../ci/config/external/file/component_spec.rb | 32 +-- spec/lib/gitlab/ci/config/yaml/loader_spec.rb | 32 +++ spec/lib/gitlab/ci/config/yaml/result_spec.rb | 55 +++-- .../parsers/sbom/cyclonedx_properties_spec.rb | 36 +-- .../sbom/source/container_scanning_spec.rb | 45 ---- .../github_import/bulk_importing_spec.rb | 18 +- spec/lib/gitlab/github_import/client_spec.rb | 6 +- .../attachments/issues_importer_spec.rb | 4 +- .../merge_requests_importer_spec.rb | 4 +- .../attachments/notes_importer_spec.rb | 4 +- .../attachments/releases_importer_spec.rb | 4 +- .../importer/labels_importer_spec.rb | 5 +- .../importer/milestones_importer_spec.rb | 5 +- .../importer/pull_requests_importer_spec.rb | 2 +- .../importer/releases_importer_spec.rb | 2 +- .../import_export/attributes_finder_spec.rb | 30 ++- .../import_export/base/object_builder_spec.rb | 12 +- .../base/relation_factory_spec.rb | 18 +- .../design_repo_restorer_spec.rb | 4 +- .../fast_hash_serializer_spec.rb | 28 ++- .../merge_request_parser_spec.rb | 10 +- .../project/object_builder_spec.rb | 150 ++++++------ .../project/tree_restorer_spec.rb | 45 ++-- spec/lib/gitlab/import_export/shared_spec.rb | 12 +- .../snippet_repo_restorer_spec.rb | 10 +- .../snippets_repo_restorer_spec.rb | 4 +- .../ci/catalog/components_project_spec.rb | 104 +++++++++ spec/models/integrations/pushover_spec.rb | 8 + spec/models/ml/model_spec.rb | 20 ++ spec/models/project_spec.rb | 11 + .../project_policy_table_shared_context.rb | 25 ++ .../auto_devops/disable_worker_spec.rb | 1 + .../import_gist_worker_spec.rb | 6 +- .../start_import_worker_spec.rb | 2 +- .../integrations/execute_worker_spec.rb | 1 + .../workers/partition_creation_worker_spec.rb | 2 +- .../projects/after_import_worker_spec.rb | 2 +- .../projects/delete_branch_worker_spec.rb | 1 + spec/workers/web_hook_worker_spec.rb | 1 + 138 files changed, 2035 insertions(+), 975 deletions(-) rename app/assets/javascripts/crm/organizations/components/graphql/{create_organization.mutation.graphql => create_customer_relations_organization.mutation.graphql} (64%) delete mode 100644 app/assets/javascripts/import_entities/components/group_dropdown.vue create mode 100644 app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue create mode 100644 app/assets/javascripts/organizations/new/components/app.vue create mode 100644 app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql create mode 100644 app/assets/javascripts/organizations/new/graphql/typedefs.graphql create mode 100644 app/assets/javascripts/organizations/new/index.js create mode 100644 app/assets/javascripts/organizations/shared/components/new_edit_form.vue create mode 100644 app/assets/javascripts/pages/organizations/organizations/new/index.js create mode 100644 app/models/ci/catalog/components_project.rb create mode 100644 config/feature_flags/ops/global_search_epics_tab.yml create mode 100644 doc/architecture/blueprints/bundle_uri/index.md delete mode 100644 lib/gitlab/ci/parsers/sbom/source/base_source.rb delete mode 100644 lib/gitlab/ci/parsers/sbom/source/container_scanning.rb delete mode 100644 spec/frontend/import_entities/components/group_dropdown_spec.js create mode 100644 spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js create mode 100644 spec/frontend/organizations/new/components/app_spec.js create mode 100644 spec/frontend/organizations/shared/components/new_edit_form_spec.js delete mode 100644 spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb create mode 100644 spec/models/ci/catalog/components_project_spec.rb diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 98fa7fd4622..3b74556bd80 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -1055,7 +1055,6 @@ Layout/ArgumentAlignment: - 'ee/spec/lib/gitlab/geo_spec.rb' - 'ee/spec/lib/gitlab/git_access_spec.rb' - 'ee/spec/lib/gitlab/git_access_wiki_spec.rb' - - 'ee/spec/lib/gitlab/import_export/project/object_builder_spec.rb' - 'ee/spec/lib/gitlab/insights/executors/dora_executor_spec.rb' - 'ee/spec/lib/gitlab/insights/loader_spec.rb' - 'ee/spec/lib/gitlab/license_scanning/branch_components_spec.rb' @@ -1568,17 +1567,6 @@ Layout/ArgumentAlignment: - 'spec/lib/gitlab/graphql/query_analyzers/ast/logger_analyzer_spec.rb' - 'spec/lib/gitlab/health_checks/redis_spec.rb' - 'spec/lib/gitlab/i18n/po_linter_spec.rb' - - 'spec/lib/gitlab/import_export/attributes_finder_spec.rb' - - 'spec/lib/gitlab/import_export/base/object_builder_spec.rb' - - 'spec/lib/gitlab/import_export/base/relation_factory_spec.rb' - - 'spec/lib/gitlab/import_export/design_repo_restorer_spec.rb' - - 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb' - - 'spec/lib/gitlab/import_export/merge_request_parser_spec.rb' - - 'spec/lib/gitlab/import_export/project/object_builder_spec.rb' - - 'spec/lib/gitlab/import_export/project/tree_restorer_spec.rb' - - 'spec/lib/gitlab/import_export/shared_spec.rb' - - 'spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb' - - 'spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb' - 'spec/lib/gitlab/instrumentation_helper_spec.rb' - 'spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb' - 'spec/lib/gitlab/markdown_cache/redis/extension_spec.rb' diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml index 673ffc92a47..2fbb94bf41b 100644 --- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml +++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml @@ -125,12 +125,6 @@ Layout/EmptyLineAfterMagicComment: - 'app/uploaders/packages/rpm/repository_file_uploader.rb' - 'app/validators/json_schema_validator.rb' - 'app/views/shared/issuable/_issuable.atom.builder' - - 'app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb' - - 'app/workers/concerns/auto_devops_queue.rb' - - 'app/workers/concerns/chaos_queue.rb' - - 'app/workers/concerns/limited_capacity/job_tracker.rb' - - 'app/workers/database/batched_background_migration/ci_database_worker.rb' - - 'app/workers/gitlab/import/stuck_project_import_jobs_worker.rb' - 'config/application.rb' - 'config/initializers/fog_core_patch.rb' - 'config/initializers/rubyzip.rb' @@ -247,8 +241,6 @@ Layout/EmptyLineAfterMagicComment: - 'ee/app/services/protected_environments/update_service.rb' - 'ee/app/services/users/captcha_challenge_service.rb' - 'ee/app/services/vulnerabilities/manually_create_service.rb' - - 'ee/app/workers/ee/issuable_export_csv_worker.rb' - - 'ee/app/workers/groups/export_memberships_worker.rb' - 'ee/db/fixtures/development/25_downstream_pipelines.rb' - 'ee/db/geo/migrate/20220617125507_create_ci_secure_file_registry.rb' - 'ee/lib/compliance_management/merge_request_approval_settings/resolver.rb' @@ -410,8 +402,6 @@ Layout/EmptyLineAfterMagicComment: - 'ee/spec/services/protected_environments/update_service_spec.rb' - 'ee/spec/services/wikis/create_attachment_service_spec.rb' - 'ee/spec/support/helpers/board_helpers.rb' - - 'ee/spec/workers/app_sec/dast/profile_schedule_worker_spec.rb' - - 'ee/spec/workers/namespaces/free_user_cap/backfill_notification_jobs_worker_spec.rb' - 'lib/api/commits.rb' - 'lib/api/concerns/packages/nuget_endpoints.rb' - 'lib/api/go_proxy.rb' @@ -800,8 +790,3 @@ Layout/EmptyLineAfterMagicComment: - 'spec/views/shared/_label_row.html.haml_spec.rb' - 'spec/views/shared/ssh_keys/_key_delete.html.haml_spec.rb' - 'spec/views/shared/wikis/_sidebar.html.haml_spec.rb' - - 'spec/workers/auto_devops/disable_worker_spec.rb' - - 'spec/workers/integrations/execute_worker_spec.rb' - - 'spec/workers/partition_creation_worker_spec.rb' - - 'spec/workers/projects/delete_branch_worker_spec.rb' - - 'spec/workers/web_hook_worker_spec.rb' diff --git a/app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql similarity index 64% rename from app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql rename to app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql index 2cc7e53ee9b..8e019420eb7 100644 --- a/app/assets/javascripts/crm/organizations/components/graphql/create_organization.mutation.graphql +++ b/app/assets/javascripts/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql @@ -1,6 +1,6 @@ #import "./crm_organization_fields.fragment.graphql" -mutation createOrganization($input: CustomerRelationsOrganizationCreateInput!) { +mutation createCustomerRelationsOrganization($input: CustomerRelationsOrganizationCreateInput!) { customerRelationsOrganizationCreate(input: $input) { organization { ...OrganizationFragment diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue index 4d2a038458d..fb056e4fa2c 100644 --- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue +++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue @@ -4,7 +4,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_CRM_ORGANIZATION, TYPENAME_GROUP } from '~/graphql_shared/constants'; import CrmForm from '../../components/crm_form.vue'; import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql'; -import createOrganizationMutation from './graphql/create_organization.mutation.graphql'; +import createCustomerRelationsOrganizationMutation from './graphql/create_customer_relations_organization.mutation.graphql'; import updateOrganizationMutation from './graphql/update_organization.mutation.graphql'; export default { @@ -31,7 +31,7 @@ export default { mutation() { if (this.isEditMode) return updateOrganizationMutation; - return createOrganizationMutation; + return createCustomerRelationsOrganizationMutation; }, getQuery() { return { diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js index 4e330ab16a8..67a472dac93 100644 --- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js +++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js @@ -73,7 +73,7 @@ export default { k8sServices(_, { configuration, namespace }) { const coreV1Api = new CoreV1Api(new Configuration(configuration)); const servicesApi = namespace - ? coreV1Api.listCoreV1NamespacedService(namespace) + ? coreV1Api.listCoreV1NamespacedService({ namespace }) : coreV1Api.listCoreV1ServiceForAllNamespaces(); return servicesApi diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue deleted file mode 100644 index 68bdcf7ef90..00000000000 --- a/app/assets/javascripts/import_entities/components/group_dropdown.vue +++ /dev/null @@ -1,72 +0,0 @@ - - diff --git a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue index b18a106608a..47c030bf1fc 100644 --- a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue +++ b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue @@ -26,13 +26,19 @@ export default { }, props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, selected: { type: String, required: true, }, userNamespace: { type: String, - required: true, + required: false, + default: undefined, }, }, @@ -66,6 +72,10 @@ export default { }, computed: { + isProject() { + return Boolean(this.userNamespace); + }, + filteredNamespaces() { return (this.namespaces ?? []).filter((ns) => ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()), @@ -78,14 +88,33 @@ export default { items() { return [ - { - text: __('Users'), - options: [{ text: this.userNamespace, value: this.userNamespace }], - }, + this.isProject + ? { + text: __('Users'), + options: [ + { + text: this.userNamespace, + value: this.userNamespace, + }, + ], + } + : { + text: __('Parent'), + textSrOnly: true, + options: [ + { + text: s__('BulkImport|No parent'), + value: '', + }, + ], + }, { text: __('Groups'), options: this.filteredNamespaces.map((namespace) => { - return { text: namespace.fullPath, value: namespace.fullPath }; + return { + text: namespace.fullPath, + value: namespace.fullPath, + }; }), }, ]; @@ -94,7 +123,15 @@ export default { methods: { onSelect(value) { - this.$emit('select', value); + if (this.isProject) { + this.$emit('select', value); + } else if (value === '') { + this.$emit('select', { fullPath: '', id: null }); + } else { + const { fullPath, id } = this.filteredNamespaces.find((ns) => ns.fullPath === value); + + this.$emit('select', { fullPath, id }); + } }, onSearch(value) { @@ -107,12 +144,13 @@ export default { diff --git a/app/assets/stylesheets/page_bundles/organizations.scss b/app/assets/stylesheets/page_bundles/organizations.scss index 7591be064c6..1f1d127a82a 100644 --- a/app/assets/stylesheets/page_bundles/organizations.scss +++ b/app/assets/stylesheets/page_bundles/organizations.scss @@ -4,3 +4,7 @@ .organization-row .organization-description p { @include gl-mb-0; } + +.organization-root-path { + max-width: 40vw; +} diff --git a/app/components/projects/ml/models_index_component.rb b/app/components/projects/ml/models_index_component.rb index 8569d26945c..57900165ad1 100644 --- a/app/components/projects/ml/models_index_component.rb +++ b/app/components/projects/ml/models_index_component.rb @@ -25,6 +25,7 @@ module Projects { name: m.name, version: m.latest_version_name, + version_count: m.version_count, path: m.latest_package_path } end diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb index 0ae99782cd3..585b35981a6 100644 --- a/app/finders/concerns/packages/finder_helper.rb +++ b/app/finders/concerns/packages/finder_helper.rb @@ -13,11 +13,13 @@ module Packages project.packages.installable end - def packages_visible_to_user(user, within_group:) + def packages_visible_to_user(user, within_group:, with_package_registry_enabled: false) return ::Packages::Package.none unless within_group return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group) projects = projects_visible_to_reporters(user, within_group: within_group) + projects = projects.with_package_registry_enabled if with_package_registry_enabled + ::Packages::Package.for_projects(projects.select(:id)).installable end diff --git a/app/finders/packages/npm/packages_for_user_finder.rb b/app/finders/packages/npm/packages_for_user_finder.rb index f42e49f9184..dc1d3b6e7fe 100644 --- a/app/finders/packages/npm/packages_for_user_finder.rb +++ b/app/finders/packages/npm/packages_for_user_finder.rb @@ -3,6 +3,8 @@ module Packages module Npm class PackagesForUserFinder < ::Packages::GroupOrProjectPackageFinder + extend ::Gitlab::Utils::Override + def execute packages end @@ -13,6 +15,11 @@ module Packages base.npm .with_name(@params[:package_name]) end + + override :group_packages + def group_packages + packages_visible_to_user(@current_user, within_group: @project_or_group, with_package_registry_enabled: true) + end end end end diff --git a/app/finders/projects/ml/model_finder.rb b/app/finders/projects/ml/model_finder.rb index 004fd20403d..1e407ba4aa4 100644 --- a/app/finders/projects/ml/model_finder.rb +++ b/app/finders/projects/ml/model_finder.rb @@ -11,6 +11,7 @@ module Projects ::Ml::Model .by_project(@project) .including_latest_version + .with_version_count end end end diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb index a88be976337..510561ec614 100644 --- a/app/helpers/integrations_helper.rb +++ b/app/helpers/integrations_helper.rb @@ -30,10 +30,6 @@ module IntegrationsHelper _("Alert") when "incident" _("Incident") - when "group_mention" - _("Group mention in public") - when "group_confidential_mention" - _("Group mention in private") end end # rubocop:enable Metrics/CyclomaticComplexity @@ -295,10 +291,6 @@ module IntegrationsHelper s_("ProjectService|Trigger event when a new, unique alert is recorded.") when "incident", "incident_events" s_("ProjectService|Trigger event when an incident is created.") - when "group_mention" - s_("ProjectService|Trigger event when a group is mentioned in a public context.") - when "group_confidential_mention" - s_("ProjectService|Trigger event when a group is mentioned in a confidential context.") when "build_events" s_("ProjectService|Trigger event when a build is created.") when "archive_trace_events" diff --git a/app/helpers/organizations/organization_helper.rb b/app/helpers/organizations/organization_helper.rb index 0f760d87173..5d89bb93000 100644 --- a/app/helpers/organizations/organization_helper.rb +++ b/app/helpers/organizations/organization_helper.rb @@ -16,6 +16,13 @@ module Organizations }.merge(shared_groups_and_projects_app_data).to_json end + def organization_new_app_data + { + organizations_path: organizations_path, + root_url: root_url + }.to_json + end + def organization_groups_and_projects_app_data shared_groups_and_projects_app_data.to_json end diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb new file mode 100644 index 00000000000..2bc33a6f050 --- /dev/null +++ b/app/models/ci/catalog/components_project.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Ci + module Catalog + class ComponentsProject + # ComponentsProject is a type of Catalog Resource which contains one or more + # CI/CD components. + # It is responsible for retrieving the data of a component file, including the content, name, and file path. + + TEMPLATE_FILE = 'template.yml' + TEMPLATES_DIR = 'templates' + TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$' + + ComponentData = Struct.new(:content, :path, keyword_init: true) + + def initialize(project, sha = project&.default_branch) + @project = project + @sha = sha + end + + def fetch_component_paths(sha) + project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha) + end + + def extract_component_name(path) + return unless path.match?(TEMPLATE_PATH_REGEX) + + dirname = File.dirname(path) + filename = File.basename(path, '.*') + + if dirname == TEMPLATES_DIR + filename + else + File.basename(dirname) + end + end + + def extract_inputs(blob) + result = Gitlab::Ci::Config::Yaml::Loader.new(blob).load_uninterpolated_yaml + + raise result.error_class, result.error unless result.valid? + + result.inputs + end + + def fetch_component(component_name) + path = simple_template_path(component_name) + content = fetch_content(path) + + if content.nil? + path = complex_template_path(component_name) + content = fetch_content(path) + end + + if content.nil? + path = legacy_template_path(component_name) + content = fetch_content(path) + end + + ComponentData.new(content: content, path: path) + end + + private + + attr_reader :project, :sha + + def fetch_content(component_path) + project.repository.blob_data_at(sha, component_path) + end + + # A simple template consists of a single file + def simple_template_path(component_name) + # TODO: Extract this line and move to fetch_content once we remove legacy fetching + return unless component_name.index('/').nil? + + File.join(TEMPLATES_DIR, "#{component_name}.yml") + end + + # A complex template is directory-based and may consist of multiple files. + # Given a path like "my-org/sub-group/the-project/templates/component" + # returns the entry point path: "templates/component/template.yml". + def complex_template_path(component_name) + # TODO: Extract this line and move to fetch_content once we remove legacy fetching + return unless component_name.index('/').nil? + + File.join(TEMPLATES_DIR, component_name, TEMPLATE_FILE) + end + + def legacy_template_path(component_name) + File.join(component_name, TEMPLATE_FILE).delete_prefix('/') + end + end + end +end diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index e97c7e5e738..2feae29f627 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -125,5 +125,9 @@ module Integrations Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data) end + + def avatar_url + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg') + end end end diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb index 0680bb0d381..27f03ed5857 100644 --- a/app/models/ml/model.rb +++ b/app/models/ml/model.rb @@ -19,6 +19,11 @@ module Ml has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model scope :including_latest_version, -> { includes(:latest_version) } + scope :with_version_count, -> { + left_outer_joins(:versions) + .select("ml_models.*, count(ml_model_versions.id) as version_count") + .group(:id) + } scope :by_project, ->(project) { where(project_id: project.id) } def valid_default_experiment? diff --git a/app/models/project.rb b/app/models/project.rb index 6c12c85d45d..fd226d23e77 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -753,6 +753,7 @@ class Project < ApplicationRecord scope :service_desk_enabled, -> { where(service_desk_enabled: true) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) } + scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) } scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) } scope :with_issues_or_mrs_available_for_user, -> (user) do diff --git a/app/services/ci/components/fetch_service.rb b/app/services/ci/components/fetch_service.rb index 45abb415174..4f09d47b530 100644 --- a/app/services/ci/components/fetch_service.rb +++ b/app/services/ci/components/fetch_service.rb @@ -5,8 +5,6 @@ module Ci class FetchService include Gitlab::Utils::StrongMemoize - TEMPLATE_FILE = 'template.yml' - COMPONENT_PATHS = [ ::Gitlab::Ci::Components::InstancePath ].freeze @@ -23,11 +21,16 @@ module Ci reason: :unsupported_path) end - component_path = component_path_class.new(address: address, content_filename: TEMPLATE_FILE) - content = component_path.fetch_content!(current_user: current_user) + component_path = component_path_class.new(address: address) + result = component_path.fetch_content!(current_user: current_user) - if content.present? - ServiceResponse.success(payload: { content: content, path: component_path }) + if result + ServiceResponse.success(payload: { + content: result.content, + path: result.path, + project: component_path.project, + sha: component_path.sha + }) else ServiceResponse.error(message: "#{error_prefix} content not found", reason: :content_not_found) end diff --git a/app/services/ml/find_or_create_model_version_service.rb b/app/services/ml/find_or_create_model_version_service.rb index 1316b2546b9..f4d3f3e72d3 100644 --- a/app/services/ml/find_or_create_model_version_service.rb +++ b/app/services/ml/find_or_create_model_version_service.rb @@ -11,7 +11,6 @@ module Ml def execute model = Ml::FindOrCreateModelService.new(project, name).execute - Ml::ModelVersion.find_or_create!(model, version, package) end diff --git a/app/views/organizations/organizations/new.html.haml b/app/views/organizations/organizations/new.html.haml index 4d7f552c87b..1a6c5a79ff6 100644 --- a/app/views/organizations/organizations/new.html.haml +++ b/app/views/organizations/organizations/new.html.haml @@ -1,3 +1,6 @@ +- add_page_specific_style 'page_bundles/organizations' - page_title s_('Organization|New organization') - header_title _("Your work"), root_path - add_to_breadcrumbs s_('Organization|Organizations'), organizations_path + +#js-organizations-new{ data: { app_data: organization_new_app_data } } diff --git a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb index 98bb259db0a..8bcbe9d6c9f 100644 --- a/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb +++ b/app/workers/ci/merge_requests/add_todo_when_build_fails_worker.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Ci module MergeRequests class AddTodoWhenBuildFailsWorker diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb index 61e3c1544bd..cdf429a8be5 100644 --- a/app/workers/concerns/auto_devops_queue.rb +++ b/app/workers/concerns/auto_devops_queue.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# + module AutoDevopsQueue extend ActiveSupport::Concern diff --git a/app/workers/concerns/chaos_queue.rb b/app/workers/concerns/chaos_queue.rb index 23e58b5182b..9a3d518dda8 100644 --- a/app/workers/concerns/chaos_queue.rb +++ b/app/workers/concerns/chaos_queue.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# + module ChaosQueue extend ActiveSupport::Concern diff --git a/app/workers/concerns/limited_capacity/job_tracker.rb b/app/workers/concerns/limited_capacity/job_tracker.rb index 4b5ce8a01f6..b4d884f914d 100644 --- a/app/workers/concerns/limited_capacity/job_tracker.rb +++ b/app/workers/concerns/limited_capacity/job_tracker.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module LimitedCapacity class JobTracker # rubocop:disable Scalability/IdempotentWorker include Gitlab::Utils::StrongMemoize diff --git a/app/workers/database/batched_background_migration/ci_database_worker.rb b/app/workers/database/batched_background_migration/ci_database_worker.rb index 58b0f5496f4..417af4c7172 100644 --- a/app/workers/database/batched_background_migration/ci_database_worker.rb +++ b/app/workers/database/batched_background_migration/ci_database_worker.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Database module BatchedBackgroundMigration class CiDatabaseWorker # rubocop:disable Scalability/IdempotentWorker diff --git a/app/workers/gitlab/github_gists_import/import_gist_worker.rb b/app/workers/gitlab/github_gists_import/import_gist_worker.rb index 60e4c8fdad6..151788150dd 100644 --- a/app/workers/gitlab/github_gists_import/import_gist_worker.rb +++ b/app/workers/gitlab/github_gists_import/import_gist_worker.rb @@ -106,9 +106,9 @@ module Gitlab def error(user_id, error_message, github_identifiers) attributes = { user_id: user_id, - github_identifiers: github_identifiers, + external_identifiers: github_identifiers, message: 'importer failed', - 'error.message': error_message + 'exception.message': error_message } Gitlab::GithubImport::Logger.error(structured_payload(attributes)) @@ -120,7 +120,7 @@ module Gitlab attributes = { user_id: user_id, message: message, - github_identifiers: gist_id + external_identifiers: gist_id } Gitlab::GithubImport::Logger.info(structured_payload(attributes)) diff --git a/app/workers/gitlab/github_gists_import/start_import_worker.rb b/app/workers/gitlab/github_gists_import/start_import_worker.rb index 33c91611719..f7d3eb1d759 100644 --- a/app/workers/gitlab/github_gists_import/start_import_worker.rb +++ b/app/workers/gitlab/github_gists_import/start_import_worker.rb @@ -51,7 +51,7 @@ module Gitlab end def log_error_and_raise!(user_id, error) - logger.error(structured_payload(user_id: user_id, message: 'import failed', 'error.message': error.message)) + logger.error(structured_payload(user_id: user_id, message: 'import failed', 'exception.message': error.message)) raise(error) end diff --git a/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb b/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb index 01979b2029f..93d670e1b8b 100644 --- a/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb +++ b/app/workers/gitlab/import/stuck_project_import_jobs_worker.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Gitlab module Import class StuckProjectImportJobsWorker # rubocop:disable Scalability/IdempotentWorker diff --git a/app/workers/projects/after_import_worker.rb b/app/workers/projects/after_import_worker.rb index 06211b2d991..47bd07d0850 100644 --- a/app/workers/projects/after_import_worker.rb +++ b/app/workers/projects/after_import_worker.rb @@ -31,7 +31,7 @@ module Projects message: 'Project housekeeping failed', project_full_path: @project.full_path, project_id: @project.id, - 'error.message' => e.message + 'exception.message' => e.message ) end diff --git a/config/feature_flags/development/print_wiki.yml b/config/feature_flags/development/print_wiki.yml index e04d7dd84bf..75305425deb 100644 --- a/config/feature_flags/development/print_wiki.yml +++ b/config/feature_flags/development/print_wiki.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414691 milestone: '16.3' type: development group: group::knowledge -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/ops/global_search_epics_tab.yml b/config/feature_flags/ops/global_search_epics_tab.yml new file mode 100644 index 00000000000..42067e9ad93 --- /dev/null +++ b/config/feature_flags/ops/global_search_epics_tab.yml @@ -0,0 +1,8 @@ +--- +name: global_search_epics_tab +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130146 +rollout_issue_url: +milestone: '16.5' +type: ops +group: group::global search +default_enabled: false diff --git a/doc/administration/audit_event_streaming/graphql_api.md b/doc/administration/audit_event_streaming/graphql_api.md index 905276a8257..6e1a3424929 100644 --- a/doc/administration/audit_event_streaming/graphql_api.md +++ b/doc/administration/audit_event_streaming/graphql_api.md @@ -338,13 +338,14 @@ To enable streaming and add a configuration, use the ```graphql mutation { - googleCloudLoggingConfigurationCreate(input: { groupPath: "my-group", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events" } ) { + googleCloudLoggingConfigurationCreate(input: { groupPath: "my-group", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "destination-name" } ) { errors googleCloudLoggingConfiguration { id googleProjectIdName logIdName clientEmail + name } errors } @@ -377,6 +378,7 @@ query { logIdName googleProjectIdName clientEmail + name } } } @@ -397,12 +399,12 @@ Prerequisite: To update streaming configuration for a top-level group, use the `googleCloudLoggingConfigurationUpdate` mutation type. You can retrieve the configuration ID -by [listing all the external destinations](#list-streaming-destinations). +by [listing all the external destinations](#list-google-cloud-logging-configurations). ```graphql mutation { googleCloudLoggingConfigurationUpdate( - input: {id: "gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events"} + input: {id: "gid://gitlab/AuditEvents::GoogleCloudLoggingConfiguration/1", googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "updated-destination-name" } ) { errors googleCloudLoggingConfiguration { @@ -410,6 +412,7 @@ mutation { logIdName googleProjectIdName clientEmail + name } } } @@ -432,7 +435,7 @@ Prerequisite: Users with the Owner role for a group can delete streaming configurations using the `googleCloudLoggingConfigurationDestroy` mutation type. You can retrieve the configurations ID -by [listing all the streaming destinations](#list-streaming-destinations) for the group. +by [listing all the streaming destinations](#list-google-cloud-logging-configurations) for the group. ```graphql mutation { @@ -453,9 +456,13 @@ Streaming configuration is deleted if: > - [Feature flag `ff_external_audit_events`](https://gitlab.com/gitlab-org/gitlab/-/issues/393772) enabled by default in GitLab 16.2. > - Instance streaming destinations [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/393772) in GitLab 16.4. [Feature flag `ff_external_audit_events`](https://gitlab.com/gitlab-org/gitlab/-/issues/417708) removed. +Manage streaming destinations for an entire instance. + +### HTTP destinations + Manage HTTP streaming destinations for an entire instance. -### Add a new HTTP destination +#### Add a new HTTP destination Add a new HTTP streaming destination to an instance. @@ -528,7 +535,7 @@ mutation { The header is created if the returned `errors` object is empty. -### List streaming destinations +#### List streaming destinations List all HTTP streaming destinations for an instance. @@ -565,7 +572,7 @@ If the resulting list is empty, then audit streaming is not enabled for the inst You need the ID values returned by this query for the update and delete mutations. -### Update streaming destinations +#### Update streaming destinations Update a HTTP streaming destination for an instance. @@ -619,7 +626,7 @@ mutation { The header is updated if the returned `errors` object is empty. -### Delete streaming destinations +#### Delete streaming destinations Delete streaming destinations for an entire instance. @@ -660,7 +667,7 @@ mutation { The header is deleted if the returned `errors` object is empty. -### Event type filters +#### Event type filters > Event type filters API [introduced](https://gitlab.com/groups/gitlab-org/-/epics/10868) in GitLab 16.2. @@ -669,7 +676,7 @@ If the feature is enabled with no filters, the destination receives all audit ev A streaming destination that has an event type filter set has a **filtered** (**{filter}**) label. -#### Use the API to add an event type filter +##### Use the API to add an event type filter Prerequisites: @@ -693,7 +700,7 @@ Event type filters are added if: - The returned `errors` object is empty. - The API responds with `200 OK`. -#### Use the API to remove an event type filter +##### Use the API to remove an event type filter Prerequisites: @@ -716,3 +723,134 @@ Event type filters are removed if: - The returned `errors` object is empty. - The API responds with `200 OK`. + +### Google Cloud Logging destinations + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/11303) in GitLab 16.5. + +Manage Google Cloud Logging destinations for an entire instance. + +Before setting up Google Cloud Logging streaming audit events, you must satisfy [the prerequisites](index.md#prerequisites). + +#### Add a new Google Cloud Logging destination + +Add a new Google Cloud Logging configuration destination to an instance. + +Prerequisites: + +- You have administrator access to the instance. +- You have a Google Cloud project with the necessary permissions to create service accounts and enable Google Cloud Logging. + +To enable streaming and add a configuration, use the +`instanceGoogleCloudLoggingConfigurationCreate` mutation in the GraphQL API. + +```graphql +mutation { + instanceGoogleCloudLoggingConfigurationCreate(input: { googleProjectIdName: "my-google-project", clientEmail: "my-email@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "destination-name" } ) { + errors + googleCloudLoggingConfiguration { + id + googleProjectIdName + logIdName + clientEmail + name + } + errors + } +} +``` + +Event streaming is enabled if: + +- The returned `errors` object is empty. +- The API responds with `200 OK`. + +#### List Google Cloud Logging configurations + +List all Google Cloud Logging configuration destinations for an instance. + +Prerequisite: + +- You have administrator access to the instance. + +You can view a list of streaming configurations for an instance using the `instanceGoogleCloudLoggingConfigurations` query +type. + +```graphql +query { + instanceGoogleCloudLoggingConfigurations { + nodes { + id + logIdName + googleProjectIdName + clientEmail + name + } + } +} +``` + +If the resulting list is empty, audit streaming is not enabled for the instance. + +You need the ID values returned by this query for the update and delete mutations. + +#### Update Google Cloud Logging configurations + +Update the Google Cloud Logging configuration destinations for an instance. + +Prerequisite: + +- You have administrator access to the instance. + +To update streaming configuration for an instance, use the +`instanceGoogleCloudLoggingConfigurationUpdate` mutation type. You can retrieve the configuration ID +by [listing all the external destinations](#list-google-cloud-logging-configurations-1). + +```graphql +mutation { + instanceGoogleCloudLoggingConfigurationUpdate( + input: {id: "gid://gitlab/AuditEvents::Instance::GoogleCloudLoggingConfiguration/1", googleProjectIdName: "updated-google-id", clientEmail: "updated@my-google-project.iam.gservice.account.com", privateKey: "YOUR_PRIVATE_KEY", logIdName: "audit-events", name: "updated name"} + ) { + errors + instanceGoogleCloudLoggingConfiguration { + id + logIdName + googleProjectIdName + clientEmail + name + } + } +} +``` + +Streaming configuration is updated if: + +- The returned `errors` object is empty. +- The API responds with `200 OK`. + +#### Delete Google Cloud Logging configurations + +Delete streaming destinations for an instance. + +When the last destination is successfully deleted, streaming is disabled for the instance. + +Prerequisite: + +- You have administrator access to the instance. + +To delete streaming configurations, use the +`instanceGoogleCloudLoggingConfigurationDestroy` mutation type. You can retrieve the configurations ID +by [listing all the streaming destinations](#list-google-cloud-logging-configurations-1) for the instance. + +```graphql +mutation { + instanceGoogleCloudLoggingConfigurationDestroy(input: { id: "gid://gitlab/AuditEvents::Instance::GoogleCloudLoggingConfiguration/1" }) { + errors + } +} +``` + +Streaming configuration is deleted if: + +- The returned `errors` object is empty. +- The API responds with `200 OK`. diff --git a/doc/administration/settings/user_and_ip_rate_limits.md b/doc/administration/settings/user_and_ip_rate_limits.md index 09ddb784191..22b16d01394 100644 --- a/doc/administration/settings/user_and_ip_rate_limits.md +++ b/doc/administration/settings/user_and_ip_rate_limits.md @@ -103,6 +103,20 @@ To use a custom response: 1. In the **Plain-text response to send to clients that hit a rate limit** text box, add the plain-text response message. +## Maximum authenticated requests to `project/:id/jobs` per minute + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129319) in GitLab 16.5. + +To reduce timeouts, the `project/:id/jobs` endpoint has a default [rate limit](../../security/rate_limits.md#project-jobs-api-endpoint) of 600 calls per authenticated user. + +To modify the maximum number of requests: + +1. On the left sidebar, select **Search or go to**. +1. Select **Admin Area**. +1. Select **Settings > Network**. +1. Expand **User and IP rate limits**. +1. Update the **Maximum authenticated requests to `project/:id/jobs` per minute** value. + ## Response headers > [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/731) in GitLab 13.8, the `RateLimit` headers. `Retry-After` was introduced in an earlier version. diff --git a/doc/api/settings.md b/doc/api/settings.md index 95d6247fea4..03877c6c489 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -121,7 +121,8 @@ Example response: "jira_connect_proxy_url": null, "silent_mode_enabled": false, "package_registry_allow_anyone_to_pull_option": true, - "bulk_import_max_download_file_size": 5120 + "bulk_import_max_download_file_size": 5120, + "project_jobs_api_rate_limit": 600 } ``` @@ -265,7 +266,8 @@ Example response: "silent_mode_enabled": false, "security_policy_global_group_approvers_enabled": true, "package_registry_allow_anyone_to_pull_option": true, - "bulk_import_max_download_file_size": 5120 + "bulk_import_max_download_file_size": 5120, + "project_jobs_api_rate_limit": 600 } ``` @@ -510,6 +512,7 @@ listed in the descriptions of the relevant settings. | `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. | | `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. | | `project_export_enabled` | boolean | no | Enable project export. | +| `project_jobs_api_rate_limit` | integer |no | Maximum authenticated requests to `/project/:id/jobs` per minute. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129319) in GitLab 16.5. Default: 600. | `projects_api_rate_limit_unauthenticated` | integer | no | [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112283) in GitLab 15.10. Max number of requests per 10 minutes per IP address for unauthenticated requests to the [list all projects API](projects.md#list-all-projects). Default: 400. To disable throttling set to 0.| | `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. | | `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. | diff --git a/doc/architecture/blueprints/bundle_uri/index.md b/doc/architecture/blueprints/bundle_uri/index.md new file mode 100644 index 00000000000..a056649a798 --- /dev/null +++ b/doc/architecture/blueprints/bundle_uri/index.md @@ -0,0 +1,216 @@ +--- +status: proposed +creation-date: "2023-08-04" +authors: [ "@toon" ] +coach: "" +approvers: [ "@mjwood", "@jcaigitlab" ] +owning-stage: "~devops::systems" +participating-stages: [] +--- + + + + +# Utilize bundle-uri to reduce Gitaly CPU load + +## Summary + +[bundle-URI](https://git-scm.com/docs/bundle-uri) is a fairly new concept +in Git that allows the client to download one or more bundles in order to +bootstrap the object database in advance of fetching the remaining objects from +a remote. By having the client download static files from a simple HTTP(S) +server in advance, the work that needs to be done on the remote side is reduced. + +Git bundles are files that store a packfile along with some extra metadata, +including a set of refs and a (possibly empty) set of necessary commits. When a +user clones a repository, the server can advertise one or more URIs that serve +these bundles. The client can download these to populate the Git object +database. After it has done this, the negotiation process between server and +client start to see which objects need be fetched. When the client pre-populated +the database with some data from the bundles, the negotiation and transfer of +objects from the server is reduced, putting less load on the server's CPU. + +## Motivation + +When a user pushes changes, it usually kicks off a CI pipeline with +a bunch of jobs. When the CI runners all clone the repository from scratch, +if they use [`git clone`](/ee/ci/pipelines/settings.md#choose-the-default-git-strategy), +they all start negotiating with the server what they need to clone. This is +really CPU intensive for the server. + +Some time ago we've introduced the +[pack-objects](/ee/administration/gitaly/configure_gitaly.md#pack-objects-cache), +but it has some pitfalls. When the tip of a branch changes, a new packfile needs +to be calculated, and the cache needs to be refreshed. + +Git bundles are more flexible. It's not a big issue if the bundle doesn't have +all the most recent objects. When it contains a fairly recent state, but is +missing the latest refs, the client (that is, the CI runner) will do a "catch up" and +fetch additional objects after applying the bundle. The set of objects it has to +fetch from will Gitaly be a lot smaller. + +### Goals + +Reduce the work that needs to be done on the Gitaly servers when a client clones +a repository. This is particularly useful for CI build farms, which generate a +lot of traffic on each commit that's pushed to the server. + +With the use bundles, the server has to craft a smaller delta packfiles +compared to the pack files that contain all the objects when no bundles are +used. This reduces the load on the CPU of the server. This has a benefit on the +packfile cache as well, because now the packfiles are smaller and faster to +generate, reducing the chances on cache misses. + +### Non-Goals + +Using bundle-URIs will **not** reduce the size of repositories stored on disk. +This feature will not be used to offload repositories, neither fully nor +partially, from the Gitaly node to some cloud storage. In contrary, because +bundles are stored elsewhere, some data is duplicated, and will cause increased +storage costs. + +In this phase it's not the goal to boost performance for incremental +fetches. When the client has already cloned the repository, bundles won't be +used to optimize fetches new data. + +Currently bundle-URI is not fully compatible with shallow clones, therefore +we'll leave that out of scope. More info about that in +[Git issue #170](https://gitlab.com/gitlab-org/git/-/issues/170). + +## Proposal + +When a client clones a repository, Gitaly advertises a bundle URI. This URI +points to a bundle that's refreshed on a regular interval, for example during +housekeeping. For each repository only one bundle will exist, so when a new one +is created, the old one is invalidated. + +The bundles will be stored on a cloud Object Storage. To use bundles, the +administrator should configure this in Gitaly. + +## Design and implementation details + +When a client initiates a `git clone`, on the server-side Gitaly spawns a +`git upload-pack` process. Gitaly can pass along additional Git +configuration. To make `git upload-pack` advertise bundle URIs, it should pass +the following configuration: + +- `uploadpack.advertiseBundleURIs` :: This should be set to `true` to enable to + use of advertised bundles. +- `bundle.version` :: At the moment only `1` is accepted. +- `bundle.mode` :: This can be either `any` or `all`. Since we only want to use + bundles for the initial clone, `any` is advised. +- `bundle..uri` :: This is the actual URI of the bundle identified with + ``. Initially we will only have one bundle per repository. + +### Enable the use of advertised bundles on the client-side + +The current version of Git does not use the advertised bundles by default when +cloning or fetching from a remote. +Luckily, we control most of the CI runners ourself. So to use bundle URI, we can +modify the Git configuration used by the runners and set +`transfer.bundleURI=true`. + +### Access control + +We don't want to leak data from private repositories through public HTTP(S) +hosts. There are a few options for how we can overcome this: + +- Only activate the use of bundle-URI on public repositories. +- Use a solution like [signed-URLs](https://cloud.google.com/cdn/docs/using-signed-urls). + +#### Public repositories only + +Gitaly itself does not know if a project, and its repository, is public, so to +determine whether bundles can be used, GitLab Rails has to tell Gitaly. It's +complex to pass this information to Gitaly, and using this approach will make +the feature only available for public projects, so we will not proceed with this +solution. + +#### Signed URLs + +The use of [signed-URLs](https://cloud.google.com/cdn/docs/using-signed-urls) is +another option to control access to the bundles. This feature, provided by +Google Cloud, allows Gitaly to create a URI that has a short lifetime. + +The downside to this approach is it depends on a feature that is +cloud-specific, so each cloud provider might provide such feature slightly +different, or not have it. But we want to roll this feature out on GitLab.com +first, which is hosted on Google Cloud, so for a first iteration we will use +this. + +### Bundle creation + +#### Use server-side backups + +At the moment Gitaly knows how to back up repositories into bundles onto cloud +storage. The [documentation](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/gitaly-backup.md#user-content-server-side-backups) +describes how to use it. + +For the initial implementation of bundle-URI we can piggy-back onto this +feature. An admin should create backups for the repositories they want to use +bundle-URI. With the existing configuration for backups, Gitaly can access cloud +storage. + +#### As part of housekeeping + +Gitaly has a housekeeping worker that daily looks for repositories to optimize. +Ideally we create a bundle right after the housekeeping (that is, garbage collection +and repacking) is done. This ensures the most optimal bundle file. + +There are a few things to keep in mind when automatically creating bundles: + +- **Does the bundle need to be recreated?** When there wasn't much activity on + the repository it's probably not needed to create a new bundle file, as the + client can fetch missing object directly from Gitaly anyway. The housekeeping + tasks uses various heuristics to determine which strategy is taken for the + housekeeping job, we can reuse parts of this logic in the creation of bundles. +- **Is it even needed to create a bundle?** Some repositories might be very + small, or see very little activity. Creating a bundle for these, and + duplicating it's data to object storage doesn't provide much value and only + generates cost and maintenance. + +#### Controlled by GitLab Rails + +Because bundles increase the cost on storage, we eventually want to give the +GitLab administrator full control over the creation of bundles. To achieve this, +bundle-URI settings will be available on the GitLab admin interface. Here the +admin can configure per project which have bundle-URI enabled. + +### Configuration + +To use this feature, Gitaly needs to be configured. For this we'll add the +following settings to Gitaly's configuration file: + +- `bundle_uri.strategy` :: This indicates which strategy should be used to + create and serve bundle-URIs. At the moment the only supported value is + "backups". When this setting to that value, Gitaly checks if a server-side + backup is available and use that. +- `bundle_uri.sign_urls` :: When set to true, the cloud storage URLs are not + passed to the client as-is, but are transformed into a signed URL. This + setting is optional and only support Google Cloud Storage (for now). + +The credentials to access cloud storage are reused as described in the Gitaly +Backups documentation. + +### Storing metadata + +For now all metadata needed to store bundles on the cloud is managed by Gitaly +server-side backups. + +### Bundle cleanup + +At some point the admin might decide to cleanup bundles for one or more +repositories, an admin command should be added for this. Because we're now only +using bundles created by `gitaly-backup`, we leave this out of scope. + +### Gitaly Cluster compatibility + +Creating server-side backups doesn't happen through Praefect at the moment. It's +up to the admin to address the nodes where they want to create backups from. If +they make sure the node is up-to-date, all nodes will have access to up-to-date +bundles and can pass proper bundle-URI parameters to the client. So no extra +work is needed to reuse server-side backup bundles with bundle-URI. + +## Alternative Solutions + +No alternative solutions are suggested at the moment. diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 7dc633a3092..860bc8790e2 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -674,6 +674,10 @@ Do not make **GitLab** possessive (GitLab's). This guidance follows [GitLab Trad ## GitLab Dedicated +Use **GitLab Dedicated** to refer to the product offering. It refers to a GitLab instance that's hosted and managed by GitLab for customers. + +GitLab Dedicated can be referred to as a single-tenant SaaS service. + Do not use **Dedicated** by itself. Always use **GitLab Dedicated**. ## GitLab Duo @@ -740,16 +744,16 @@ See also: ## GitLab SaaS -**GitLab SaaS** refers to the product license that provides access to GitLab.com. It does not refer to the -GitLab instance managed by GitLab itself. +Use **GitLab SaaS** to refer to the product offering. +It does not refer to the GitLab instance, which is [GitLab.com](#gitlabcom). ## GitLab self-managed -Use **GitLab self-managed** to refer to the product license for GitLab instances managed by customers themselves. +Use **GitLab self-managed** to refer to the product offering. It refers to a GitLab instance managed by customers themselves. ## GitLab.com -**GitLab.com** refers to the GitLab instance managed by GitLab itself. +Use **GitLab.com** to refer to the URL. GitLab.com is the instance that's managed by GitLab. ## guide @@ -1132,6 +1136,16 @@ Instead of: - Note that you can change the settings. +## offerings + +The current product offerings are: + +- [GitLab SaaS](#gitlab-saas) +- [GitLab self-managed](#gitlab-self-managed) +- [GitLab Dedicated](#gitlab-dedicated) + +The [tier badges](index.md#available-product-tier-badges) reflect these offerings. + ## older Do not use **older** when talking about version numbers. diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md index 024ed7b4f2c..2f916ec34b5 100644 --- a/doc/security/rate_limits.md +++ b/doc/security/rate_limits.md @@ -126,7 +126,7 @@ The **rate limit** is 20 calls per minute per IP address. There is a rate limit for the endpoint `project/:id/jobs`, which is enforced to reduce timeouts when retrieving jobs. -The **rate limit** is 600 calls per minute per authenticated user. +The **rate limit** defaults to 600 calls per authenticated user. You can [configure the rate limit](../administration/settings/user_and_ip_rate_limits.md). ### AI action diff --git a/doc/user/project/repository/code_suggestions/index.md b/doc/user/project/repository/code_suggestions/index.md index 0660d400b44..151792089ce 100644 --- a/doc/user/project/repository/code_suggestions/index.md +++ b/doc/user/project/repository/code_suggestions/index.md @@ -9,6 +9,7 @@ type: index, reference > - [Introduced support for Google Vertex AI Codey APIs](https://gitlab.com/groups/gitlab-org/-/epics/10562) in GitLab 16.1. > - [Removed support for GitLab native model](https://gitlab.com/groups/gitlab-org/-/epics/10752) in GitLab 16.2. +> - [Introduced support for Code Generation](https://gitlab.com/gitlab-org/gitlab/-/issues/415583) in GitLab 16.3. WARNING: This feature is in [Beta](../../../../policy/experiment-beta-support.md#beta). @@ -22,12 +23,46 @@ GitLab Duo Code Suggestions are available: - In VS Code, Microsoft Visual Studio, JetBrains IDEs, and Neovim. You must have the corresponding GitLab extension installed. - In the GitLab WebIDE. -Usage of Code Suggestions is governed by the [GitLab Testing Agreement](https://about.gitlab.com/handbook/legal/testing-agreement/). + +
+ +
+ +During Beta, usage of Code Suggestions is governed by the [GitLab Testing Agreement](https://about.gitlab.com/handbook/legal/testing-agreement/). Learn about [data usage when using Code Suggestions](#code-suggestions-data-usage). +## Use Code Suggestions + +Prerequisites: + +- Code Suggestions must be enabled for [SaaS](saas.md#enable-code-suggestions) or for [self-managed](self_managed.md#enable-code-suggestions-on-self-managed-gitlab). +- You must have installed and configured a [supported IDE editor extension](index.md#supported-editor-extensions). + +To use Code Suggestions: + +1. Author your code. As you type, suggestions are displayed. Code Suggestions, depending on the cursor position, either provides code snippets or completes the current line. +1. Describe the requirements in natural language. Be concise and specific. Code Suggestions generates functions and code snippets as appropriate. +1. To accept a suggestion, press Tab. +1. To ignore a suggestion, keep typing as you usually would. +1. To explicitly reject a suggestion, press esc. + +Things to remember: + +- AI is non-deterministic, so you may not get the same suggestion every time with the same input. +- Just like product requirements, writing clear, descriptive, and specific tasks results in quality generated code. + +### Progressive enhancement + +This feature is designed as a progressive enhancement to developer's IDEs. +Code Suggestions offer a completion if a suitable recommendation is provided to the user in a timely matter. +In the event of a connection issue or model inference failure, the feature gracefully degrades. +Code Suggestions do not prevent you from writing code in your IDE. + ## Supported languages -The best results from Code Suggestions are expected [for languages the Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_coding_languages) directly support: +The best results from Code Suggestions are expected for languages that [Anthropic Claude](https://www.anthropic.com/product) and the [Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_coding_languages) directly support: - C++ - C# @@ -44,16 +79,6 @@ The best results from Code Suggestions are expected [for languages the Google Ve - Swift - TypeScript -### Supported code infrastructure interfaces - -Code Suggestions includes [Google Vertex AI Codey APIs](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview#supported_code_infrastructure_interfaces) support for the following infrastructure as code interfaces: - -- Google Cloud CLI -- Kubernetes Resource Model (KRM) -- Terraform - -Suggestion quality for other languages and using natural language code comments to request completions may not yet result in high-quality suggestions. - ### Supported languages in IDEs Editor support for languages is documented in the following table. @@ -101,13 +126,12 @@ This improvement should result in: ## Code Suggestions data usage -Code Suggestions is a generative artificial intelligence (AI) model. +Code Suggestions is powered by a generative AI model. -Your personal access token enables a secure API connection to GitLab.com. -This API connection securely transmits a context window from your IDE/editor to the Code Suggestions GitLab hosted service which calls Google Vertex AI Codey APIs, -and the generated suggestion is transmitted back to your IDE/editor. +Your personal access token enables a secure API connection to GitLab.com or to your GitLab instance. +This API connection securely transmits a context window from your IDE/editor to the [GitLab AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist), a GitLab hosted service. The gateway calls the large language model APIs, and then the generated suggestion is transmitted back to your IDE/editor. -GitLab currently leverages [Google Cloud's Vertex AI Codey API models](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview). Learn more about Google Vertex AI Codey APIs [Data Governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance). +GitLab selects the best-in-class large-language models for specific tasks. We use [Google Vertex AI Code Models](https://cloud.google.com/vertex-ai/docs/generative-ai/code/code-models-overview) and [Anthropic Claude](https://www.anthropic.com/product) for Code Suggestions. ### Telemetry @@ -124,48 +148,16 @@ For self-managed instances that have enabled Code Suggestions and SaaS accounts, ### Inference window context -Code Suggestions currently inferences against the currently opened file and has a context window of 2,048 tokens and 8,192 character limits. This limit includes content before and after the cursor, the file name, and the extension type. -Learn more about Google Vertex AI [code-gecko](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models). - -The maximum number of tokens that is generated in the response is default 64. A token is approximately four characters. 100 tokens correspond to roughly 60-80 words. -Learn more about Google Vertex AI [`code-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-completion). +Code Suggestions inferences against the currently opened file, the content before and after the cursor, the filename, and the extension type. For more information on possible future context expansion to improve the quality of suggestions, see [epic 11669](https://gitlab.com/groups/gitlab-org/-/epics/11669). ### Training data -Code Suggestions are routed through Google Vertex AI Codey APIs. Learn more about Google Vertex AI Codey APIs [Data Governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance) and [Responsible AI](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai). +GitLab does not train generative AI models based on private (non-public) data. The vendors we work with also do not train models based on private data. -Google Vertex AI Codey APIs are not trained on private non-public GitLab customer or user data. +For more information on GitLab Code Suggestions data [sub-processors](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors), see: -Google has [shared the following](https://ai.google/discover/foundation-models/) about the data Codey models are trained on: - -> Codey is our family of foundational coding models built on PaLM 2. Codey was fine-tuned on a large dataset of high quality, permissively licensed code from external sources - -## Progressive enhancement - -This feature is designed as a progressive enhancement to developer's IDEs. -Code Suggestions offer a completion if the machine learning engine can generate a recommendation. -In the event of a connection issue or model inference failure, the feature gracefully degrades. -Code Suggestions do not prevent you from writing code in your IDE. - -### Internet connectivity - -Code Suggestions does not work with offline environments. - -To use Code Suggestions: - -- On GitLab.com, you must have an internet connection and be able to access GitLab. -- In GitLab 16.1 and later, on self-managed GitLab, you must have an internet connection. - -### Model accuracy and quality - -Code Suggestions can generate low-quality, incomplete, and possibly insecure code. -We strongly encourage all beta users to leverage GitLab native -[Code Quality Scanning](../../../../ci/testing/code_quality.md) and -[Security Scanning](../../../application_security/index.md) capabilities. - -GitLab currently does not retrain Google Vertex AI Codey APIs. GitLab makes no claims -to the accuracy or quality of Code Suggestions generated by Google Vertex AI Codey API. -Read more about [Google Vertex AI foundation model capabilities](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/models). +- Google Vertex AI Codey APIs [data governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance) and [responsible AI](https://cloud.google.com/vertex-ai/docs/generative-ai/learn/responsible-ai). +- Anthropic Claude's [constitution](https://www.anthropic.com/index/claudes-constitution). ## Known limitations @@ -178,12 +170,6 @@ However, Code Suggestions may generate suggestions that are: - Insecure code - Offensive or insensitive -We are also aware of specific situations that can produce unexpected or incoherent results including: - -- Suggestions written in the middle of existing functions, or "fill in the middle." -- Suggestions based on natural language code comments. -- Suggestions that mixed programming languages in unexpected ways. - ## Feedback Report issues in the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/405152). diff --git a/doc/user/project/repository/code_suggestions/saas.md b/doc/user/project/repository/code_suggestions/saas.md index 49221ec722d..ac64aba4335 100644 --- a/doc/user/project/repository/code_suggestions/saas.md +++ b/doc/user/project/repository/code_suggestions/saas.md @@ -38,19 +38,4 @@ Prerequisites: - Ensure Code Suggestions is enabled for your user and group. - You must have installed and configured a [supported IDE editor extension](index.md#supported-editor-extensions). -To use Code Suggestions: - -1. Author your code. As you type, suggestions are displayed. Depending on the cursor position, the extension either: - - - Provides entire code snippets, like generating functions. - - Completes the current line. - -1. To accept a suggestion, press Tab. - -Suggestions are best when writing new code. Editing existing functions or 'fill in the middle' of a function may not perform as expected. - -GitLab is making improvements to the Code Suggestions to improve the quality. AI is non-deterministic, so you may not get the same suggestion every time with the same input. - -This feature is currently in [Beta](../../../../policy/experiment-beta-support.md#beta). -Code Suggestions depends on both Google Vertex AI Codey APIs and the GitLab Code Suggestions service. We have built this feature to gracefully degrade and have controls in place to allow us to -mitigate abuse or misuse. GitLab may disable this feature for any or all customers at any time at our discretion. +[Use Code Suggestions](index.md#use-code-suggestions). diff --git a/doc/user/project/repository/code_suggestions/self_managed.md b/doc/user/project/repository/code_suggestions/self_managed.md index 3c149604086..ee501212027 100644 --- a/doc/user/project/repository/code_suggestions/self_managed.md +++ b/doc/user/project/repository/code_suggestions/self_managed.md @@ -156,22 +156,7 @@ Prerequisites: - Code Suggestions must be enabled [for the instance](#enable-code-suggestions-on-self-managed-gitlab). - You must have installed and configured a [supported IDE editor extension](index.md#supported-editor-extensions). -To use Code Suggestions: - -1. Author your code. As you type, suggestions are displayed. Depending on the cursor position, the extension either: - - - Provides entire code snippets, like generating functions. - - Completes the current line. - -1. To accept a suggestion, press Tab. - -Suggestions are best when writing new code. Editing existing functions or 'fill in the middle' of a function may not perform as expected. - -GitLab is making improvements to the Code Suggestions to improve the quality. AI is non-deterministic, so you may not get the same suggestion every time with the same input. - -This feature is currently in [Beta](../../../../policy/experiment-beta-support.md#beta). -Code Suggestions depends on both Google Vertex AI Codey APIs and the GitLab Code Suggestions service. We have built this feature to gracefully degrade and have controls in place to allow us to -mitigate abuse or misuse. GitLab may disable this feature for any or all customers at any time at our discretion. +[Use Code Suggestions](index.md#use-code-suggestions). ### Data privacy diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 8c7db5ca29e..e8dfbfa675a 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -24,6 +24,7 @@ by disabling one or more [`ops` feature flags](../../development/feature_flags/i |----------------|------------------------------------|-------------------------------------------------------------------------------------------| | Code | `global_search_code_tab` | When enabled, global search includes code. | | Commits | `global_search_commits_tab` | When enabled, global search includes commits. | +| Epics | `global_search_epics_tab` | When enabled, global search includes epics. | | Issues | `global_search_issues_tab` | When enabled, global search includes issues. | | Merge requests | `global_search_merge_requests_tab` | When enabled, global search includes merge requests. | | Users | `global_search_users_tab` | When enabled, global search includes users. | diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb index 17c784c4d54..551284d9099 100644 --- a/lib/gitlab/ci/components/instance_path.rb +++ b/lib/gitlab/ci/components/instance_path.rb @@ -7,19 +7,17 @@ module Gitlab include Gitlab::Utils::StrongMemoize LATEST_VERSION_KEYWORD = '~latest' - TEMPLATES_DIR = 'templates' def self.match?(address) address.include?('@') && address.start_with?(Settings.gitlab_ci['component_fqdn']) end - attr_reader :host, :project_file_path + attr_reader :host - def initialize(address:, content_filename:) + def initialize(address:) @full_path, @version = address.to_s.split('@', 2) - @content_filename = content_filename @host = Settings.gitlab_ci['component_fqdn'] - @project_file_path = nil + @component_project = ::Ci::Catalog::ComponentsProject.new(project, sha) end def fetch_content!(current_user:) @@ -28,7 +26,7 @@ module Gitlab raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project) - content(simple_template_path) || content(complex_template_path) || content(legacy_template_path) + @component_project.fetch_component(component_name) end def project @@ -46,16 +44,7 @@ module Gitlab private - attr_reader :version, :path - - def instance_path - @full_path.delete_prefix(host) - end - - def component_path - instance_path.delete_prefix(project.full_path).delete_prefix('/') - end - strong_memoize_attr :component_path + attr_reader :version # Given a path like "my-org/sub-group/the-project/path/to/component" # find the project "my-org/sub-group/the-project" by looking at all possible paths. @@ -65,46 +54,24 @@ module Gitlab while index = path.rindex('/') # find index of last `/` in a path possible_paths << (path = path[0..index - 1]) end - # remove shortest path as it is group possible_paths.pop ::Project.where_full_path_in(possible_paths).take # rubocop: disable CodeReuse/ActiveRecord end + def instance_path + @full_path.delete_prefix(host) + end + + def component_name + instance_path.delete_prefix(project.full_path).delete_prefix('/') + end + strong_memoize_attr :component_name + def latest_version_sha project.releases.latest&.sha end - - # A simple template consists of a single file - def simple_template_path - # Extract this line and move to fetch_content once we remove legacy fetching - return unless templates_dir_exists? && component_path.index('/').nil? - - @project_file_path = File.join(TEMPLATES_DIR, "#{component_path}.yml") - end - - # A complex template is directory-based and may consist of multiple files. - # Given a path like "my-org/sub-group/the-project/templates/component" - # returns the entry point path: "templates/component/template.yml". - def complex_template_path - # Extract this line and move to fetch_content once we remove legacy fetching - return unless templates_dir_exists? && component_path.index('/').nil? - - @project_file_path = File.join(TEMPLATES_DIR, component_path, @content_filename) - end - - def legacy_template_path - @project_file_path = File.join(component_path, @content_filename).delete_prefix('/') - end - - def templates_dir_exists? - project.repository.tree.trees.map(&:name).include?(TEMPLATES_DIR) - end - - def content(path) - project.repository.blob_data_at(sha, path) - end end end end diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb index de6de1bb7a8..03063e76dde 100644 --- a/lib/gitlab/ci/config/external/file/component.rb +++ b/lib/gitlab/ci/config/external/file/component.rb @@ -20,7 +20,7 @@ module Gitlab ::Gitlab::UsageDataCounters::HLLRedisCounter.track_event('cicd_component_usage', values: context.user.id) - component_result.payload.fetch(:content) + component_payload.fetch(:content) end strong_memoize_attr :content @@ -65,30 +65,30 @@ module Gitlab override :expand_context_attrs def expand_context_attrs { - project: component_path.project, - sha: component_path.sha, + project: component_payload.fetch(:project), + sha: component_payload.fetch(:sha), user: context.user, variables: context.variables } end def masked_blob - return unless component_path + return unless component_payload context.mask_variables_from( Gitlab::Routing.url_helpers.project_blob_url( - component_path.project, - ::File.join(component_path.sha, component_path.project_file_path)) + component_payload.fetch(:project), + ::File.join(component_payload.fetch(:sha), component_payload.fetch(:path))) ) end strong_memoize_attr :masked_blob - def component_path + def component_payload return unless component_result.success? - component_result.payload.fetch(:path) + component_result.payload end - strong_memoize_attr :component_path + strong_memoize_attr :component_payload end end end diff --git a/lib/gitlab/ci/config/yaml/loader.rb b/lib/gitlab/ci/config/yaml/loader.rb index c659ad5b8d1..1e9ac2b3dd5 100644 --- a/lib/gitlab/ci/config/yaml/loader.rb +++ b/lib/gitlab/ci/config/yaml/loader.rb @@ -33,16 +33,16 @@ module Gitlab end end - private - - attr_reader :content, :inputs, :variables - def load_uninterpolated_yaml Yaml::Result.new(config: load_yaml!, error: nil) rescue ::Gitlab::Config::Loader::FormatError => e Yaml::Result.new(error: e.message, error_class: e) end + private + + attr_reader :content, :inputs, :variables + def load_yaml! ensure_custom_tags diff --git a/lib/gitlab/ci/config/yaml/result.rb b/lib/gitlab/ci/config/yaml/result.rb index a68cfde6653..0e7e9230467 100644 --- a/lib/gitlab/ci/config/yaml/result.rb +++ b/lib/gitlab/ci/config/yaml/result.rb @@ -39,6 +39,10 @@ module Gitlab @config.first || {} end + + def inputs + (has_header? && header[:spec][:inputs]) || {} + end end end end diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb index 35548358c57..3dc73544208 100644 --- a/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb +++ b/lib/gitlab/ci/parsers/sbom/cyclonedx_properties.rb @@ -15,8 +15,7 @@ module Gitlab SUPPORTED_SCHEMA_VERSION = '1' GITLAB_PREFIX = 'gitlab:' SOURCE_PARSERS = { - 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning, - 'container_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning + 'dependency_scanning' => ::Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning }.freeze SUPPORTED_PROPERTIES = %w[ meta:schema_version @@ -25,10 +24,6 @@ module Gitlab dependency_scanning:source_file:path dependency_scanning:package_manager:name dependency_scanning:language:name - container_scanning:image:name - container_scanning:image:tag - container_scanning:operating_system:name - container_scanning:operating_system:version ].freeze def self.parse_source(...) diff --git a/lib/gitlab/ci/parsers/sbom/source/base_source.rb b/lib/gitlab/ci/parsers/sbom/source/base_source.rb deleted file mode 100644 index 744555aa25a..00000000000 --- a/lib/gitlab/ci/parsers/sbom/source/base_source.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Parsers - module Sbom - module Source - class BaseSource - REQUIRED_ATTRIBUTES = [].freeze - - def self.source(...) - new(...).source - end - - def initialize(data) - @data = data - end - - def source - return unless required_attributes_present? - - ::Gitlab::Ci::Reports::Sbom::Source.new( - type: type, - data: data - ) - end - - private - - attr_reader :data - - # Implement in child class - # returns a symbol of the source type - def type; end - - def required_attributes_present? - self.class::REQUIRED_ATTRIBUTES.all? do |keys| - data.dig(*keys).present? - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb deleted file mode 100644 index eaa3b0db177..00000000000 --- a/lib/gitlab/ci/parsers/sbom/source/container_scanning.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Parsers - module Sbom - module Source - class ContainerScanning < BaseSource - REQUIRED_ATTRIBUTES = [ - %w[image name], - %w[image tag], - %w[operating_system name], - %w[operating_system version] - ].freeze - - private - - def type - :container_scanning - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb index fc5a7606e39..c76a4309779 100644 --- a/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb +++ b/lib/gitlab/ci/parsers/sbom/source/dependency_scanning.rb @@ -5,15 +5,36 @@ module Gitlab module Parsers module Sbom module Source - class DependencyScanning < BaseSource + class DependencyScanning REQUIRED_ATTRIBUTES = [ %w[input_file path] ].freeze + def self.source(...) + new(...).source + end + + def initialize(data) + @data = data + end + + def source + return unless required_attributes_present? + + ::Gitlab::Ci::Reports::Sbom::Source.new( + type: :dependency_scanning, + data: data + ) + end + private - def type - :dependency_scanning + attr_reader :data + + def required_attributes_present? + REQUIRED_ATTRIBUTES.all? do |keys| + data.dig(*keys).present? + end end end end diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb index d16f4d7587b..47080ea1979 100644 --- a/lib/gitlab/github_import/bulk_importing.rb +++ b/lib/gitlab/github_import/bulk_importing.rb @@ -32,7 +32,7 @@ module Gitlab log_error(github_identifiers, build_record.errors.full_messages) errors << { validation_errors: build_record.errors, - github_identifiers: github_identifiers + external_identifiers: github_identifiers } next end @@ -69,7 +69,7 @@ module Gitlab correlation_id_value: correlation_id_value, retry_count: nil, created_at: Time.zone.now, - external_identifiers: error[:github_identifiers] + external_identifiers: error[:external_identifiers] } end @@ -79,8 +79,7 @@ module Gitlab private def log_and_increment_counter(value, operation) - Gitlab::Import::Logger.info( - import_type: :github, + Logger.info( project_id: project.id, importer: self.class.name, message: "#{value} #{object_type.to_s.pluralize} #{operation}" @@ -95,12 +94,11 @@ module Gitlab end def log_error(github_identifiers, messages) - Gitlab::Import::Logger.error( - import_type: :github, + Logger.error( project_id: project.id, importer: self.class.name, message: messages, - github_identifiers: github_identifiers + external_identifiers: github_identifiers ) end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 23d4faa3dde..5a0ae680ab8 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -284,10 +284,10 @@ module Gitlab def on_retry proc do |exception, try, elapsed_time, next_interval| - Gitlab::Import::Logger.info( + Logger.info( message: "GitHub connection retry triggered", 'error.class': exception.class, - 'error.message': exception.message, + 'exception.message': exception.message, try_count: try, elapsed_time_s: elapsed_time, wait_to_retry_s: next_interval diff --git a/lib/gitlab/github_import/importer/attachments/issues_importer.rb b/lib/gitlab/github_import/importer/attachments/issues_importer.rb index c8f0b59fd18..a0e1a3f2d25 100644 --- a/lib/gitlab/github_import/importer/attachments/issues_importer.rb +++ b/lib/gitlab/github_import/importer/attachments/issues_importer.rb @@ -24,7 +24,7 @@ module Gitlab private def collection - project.issues.select(:id, :description, :iid) + project.issues.id_not_in(already_imported_ids).select(:id, :description, :iid) end def ordering_column diff --git a/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb b/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb index cd3a327a846..22b3e7c640b 100644 --- a/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb +++ b/lib/gitlab/github_import/importer/attachments/merge_requests_importer.rb @@ -24,7 +24,7 @@ module Gitlab private def collection - project.merge_requests.select(:id, :description, :iid) + project.merge_requests.id_not_in(already_imported_ids).select(:id, :description, :iid) end def ordering_column diff --git a/lib/gitlab/github_import/importer/attachments/notes_importer.rb b/lib/gitlab/github_import/importer/attachments/notes_importer.rb index aa38a7a3a3f..5ab0cf5b6b0 100644 --- a/lib/gitlab/github_import/importer/attachments/notes_importer.rb +++ b/lib/gitlab/github_import/importer/attachments/notes_importer.rb @@ -26,7 +26,7 @@ module Gitlab # TODO: exclude :system, :noteable_type from select after removing override Note#note method # https://gitlab.com/gitlab-org/gitlab/-/issues/369923 def collection - project.notes.user.select(:id, :note, :system, :noteable_type) + project.notes.id_not_in(already_imported_ids).user.select(:id, :note, :system, :noteable_type) end end end diff --git a/lib/gitlab/github_import/importer/attachments/releases_importer.rb b/lib/gitlab/github_import/importer/attachments/releases_importer.rb index 7d6dbeb901e..0527170f5e1 100644 --- a/lib/gitlab/github_import/importer/attachments/releases_importer.rb +++ b/lib/gitlab/github_import/importer/attachments/releases_importer.rb @@ -24,7 +24,7 @@ module Gitlab private def collection - project.releases.select(:id, :description, :tag) + project.releases.id_not_in(already_imported_ids).select(:id, :description, :tag) end end end diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb index c2ea7ba4874..d49180e6927 100644 --- a/lib/gitlab/github_import/importer/diff_note_importer.rb +++ b/lib/gitlab/github_import/importer/diff_note_importer.rb @@ -126,7 +126,7 @@ module Gitlab Logger.info( project_id: project.id, importer: self.class.name, - github_identifiers: note.github_identifiers, + external_identifiers: note.github_identifiers, model: model ) end diff --git a/lib/gitlab/github_import/importer/pull_requests_importer.rb b/lib/gitlab/github_import/importer/pull_requests_importer.rb index 62863ba67fd..671e023e90b 100644 --- a/lib/gitlab/github_import/importer/pull_requests_importer.rb +++ b/lib/gitlab/github_import/importer/pull_requests_importer.rb @@ -44,7 +44,7 @@ module Gitlab pname = project.path_with_namespace - Gitlab::Import::Logger.info( + Logger.info( message: 'GitHub importer finished updating repository', project_name: pname ) diff --git a/lib/gitlab/github_import/parallel_scheduling.rb b/lib/gitlab/github_import/parallel_scheduling.rb index cfc1ec526b0..cccd99f48b1 100644 --- a/lib/gitlab/github_import/parallel_scheduling.rb +++ b/lib/gitlab/github_import/parallel_scheduling.rb @@ -211,6 +211,12 @@ module Gitlab private + # Returns the set used to track "already imported" objects. + # Items are the values returned by `#id_for_already_imported_cache`. + def already_imported_ids + Gitlab::Cache::Import::Caching.values_from_set(already_imported_cache_key) + end + def additional_object_data {} end diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb index 1832f071a44..4bf2d8a0aca 100644 --- a/lib/gitlab/github_import/user_finder.rb +++ b/lib/gitlab/github_import/user_finder.rb @@ -271,8 +271,7 @@ module Gitlab end def log(message, username: nil) - Gitlab::Import::Logger.info( - import_type: :github, + Logger.info( project_id: project.id, class: self.class.name, username: username, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d5fad34dd4e..d5671460bbb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8891,9 +8891,6 @@ msgstr "" msgid "BulkImport|Direct transfer maximum download file size (MiB)" msgstr "" -msgid "BulkImport|Existing groups" -msgstr "" - msgid "BulkImport|Filter by source group" msgstr "" @@ -22623,12 +22620,6 @@ msgstr "" msgid "Group membership expiration date removed" msgstr "" -msgid "Group mention in private" -msgstr "" - -msgid "Group mention in public" -msgstr "" - msgid "Group milestone" msgstr "" @@ -25268,6 +25259,12 @@ msgstr "" msgid "IntegrationEvents|A deployment is started or finished" msgstr "" +msgid "IntegrationEvents|A group is mentioned in a confidential context" +msgstr "" + +msgid "IntegrationEvents|A group is mentioned in a public context" +msgstr "" + msgid "IntegrationEvents|A merge request is created, merged, closed, or reopened" msgstr "" @@ -30434,12 +30431,20 @@ msgstr "" msgid "MlExperimentTracking|Triggered by" msgstr "" +msgid "MlModelRegistry|%{version} · No other versions" +msgid_plural "MlModelRegistry|%{version} · %{versionCount} versions" +msgstr[0] "" +msgstr[1] "" + msgid "MlModelRegistry|Model registry" msgstr "" msgid "MlModelRegistry|No models registered in this project" msgstr "" +msgid "MlModelRegistry|No registered versions" +msgstr "" + msgid "Modal updated" msgstr "" @@ -33074,9 +33079,15 @@ msgstr "" msgid "Organizations" msgstr "" +msgid "Organization|%{linkStart}Organizations%{linkEnd} are a top-level container to hold your groups and projects." +msgstr "" + msgid "Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder." msgstr "" +msgid "Organization|An error occurred creating an organization. Please try again." +msgstr "" + msgid "Organization|An error occurred loading the groups. Please refresh the page to try again." msgstr "" @@ -33092,6 +33103,9 @@ msgstr "" msgid "Organization|Create an organization to contain all of your groups and projects." msgstr "" +msgid "Organization|Create organization" +msgstr "" + msgid "Organization|Frequently visited groups" msgstr "" @@ -33104,12 +33118,30 @@ msgstr "" msgid "Organization|Manage" msgstr "" +msgid "Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses." +msgstr "" + +msgid "Organization|My organization" +msgstr "" + msgid "Organization|New organization" msgstr "" msgid "Organization|Org ID" msgstr "" +msgid "Organization|Organization URL" +msgstr "" + +msgid "Organization|Organization URL is required." +msgstr "" + +msgid "Organization|Organization name" +msgstr "" + +msgid "Organization|Organization name is required." +msgstr "" + msgid "Organization|Organization navigation" msgstr "" @@ -33134,6 +33166,9 @@ msgstr "" msgid "Organization|You don't have any projects yet." msgstr "" +msgid "Organization|my-organization" +msgstr "" + msgid "Orphaned member" msgstr "" @@ -37021,12 +37056,6 @@ msgstr "" msgid "ProjectService|Trigger event when a deployment starts or finishes." msgstr "" -msgid "ProjectService|Trigger event when a group is mentioned in a confidential context." -msgstr "" - -msgid "ProjectService|Trigger event when a group is mentioned in a public context." -msgstr "" - msgid "ProjectService|Trigger event when a merge request is created, updated, or merged." msgstr "" diff --git a/qa/qa/page/group/bulk_import.rb b/qa/qa/page/group/bulk_import.rb index 9fb262c27c3..52e5593cb26 100644 --- a/qa/qa/page/group/bulk_import.rb +++ b/qa/qa/page/group/bulk_import.rb @@ -11,12 +11,8 @@ module QA element 'filter-groups' end - view "app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue" do - element :target_group_dropdown_item - end - - view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do - element :target_namespace_selector_dropdown + view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do + element 'target-namespace-dropdown' end view "app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue" do @@ -40,8 +36,8 @@ module QA filter_group(source_group_name) within_element(:import_item, source_group: source_group_name) do - click_element(:target_namespace_selector_dropdown) - click_element(:target_group_dropdown_item, group_name: target_group_name) + click_element('target-namespace-dropdown') + click_element("listbox-item-#{target_group_name}") retry_until(message: "Triggering import") do click_element('import-group-button') diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index 9e1bf0a393f..08400042028 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -20,7 +20,7 @@ module QA end view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do - element :target_namespace_selector_dropdown + element 'target-namespace-dropdown' end # Add personal access token @@ -46,7 +46,7 @@ module QA # @return [void] def import!(gh_project_name, target_group_path, project_name) within_element(:project_import_row, source_project: gh_project_name) do - click_element(:target_namespace_selector_dropdown) + click_element('target-namespace-dropdown') click_element("listbox-item-#{target_group_path}", wait: 10) fill_element(:project_path_field, project_name) diff --git a/spec/components/projects/ml/models_index_component_spec.rb b/spec/components/projects/ml/models_index_component_spec.rb index 586190c8fc0..c42c94d5d01 100644 --- a/spec/components/projects/ml/models_index_component_spec.rb +++ b/spec/components/projects/ml/models_index_component_spec.rb @@ -30,6 +30,8 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat let(:element) { page.find("#js-index-ml-models") } before do + allow(model1).to receive(:version_count).and_return(1) + allow(model2).to receive(:version_count).and_return(0) render_inline component end @@ -41,12 +43,14 @@ RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_cat { 'name' => model1.name, 'version' => model1.latest_version.version, - 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}" + 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}", + 'versionCount' => 1 }, { 'name' => model2.name, 'version' => nil, - 'path' => nil + 'path' => nil, + 'versionCount' => 0 } ], 'pageInfo' => { diff --git a/spec/controllers/oauth/tokens_controller_spec.rb b/spec/controllers/oauth/tokens_controller_spec.rb index 3205b46c5b2..489470dc0df 100644 --- a/spec/controllers/oauth/tokens_controller_spec.rb +++ b/spec/controllers/oauth/tokens_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Oauth::TokensController do +RSpec.describe Oauth::TokensController, feature_category: :user_management do let(:user) { create(:user) } it 'includes Two-factor enforcement concern' do @@ -24,12 +24,6 @@ RSpec.describe Oauth::TokensController do end end - before do - Rails.application.routes.draw do - post 'create' => 'anonymous#create' - end - end - it 'does log correlation id' do Labkit::Correlation::CorrelationId.use_id('new-id') do post :create diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb index 94bcec6163e..f81e940c7ed 100644 --- a/spec/finders/concerns/packages/finder_helper_spec.rb +++ b/spec/finders/concerns/packages/finder_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Packages::FinderHelper do +RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do describe '#packages_for_project' do let_it_be_with_reload(:project1) { create(:project) } let_it_be(:package1) { create(:package, project: project1) } @@ -107,6 +107,34 @@ RSpec.describe ::Packages::FinderHelper do it_behaves_like params[:shared_example_name] end + + context 'when the second project has the package registry disabled' do + before do + project1.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC, + package_registry_access_level: 'disabled', packages_enabled: false) + end + + it_behaves_like 'returning both packages' + + context 'with with_package_registry_enabled set to true' do + let(:finder_class) do + Class.new do + include ::Packages::FinderHelper + + def initialize(user) + @current_user = user + end + + def execute(group) + packages_visible_to_user(@current_user, within_group: group, with_package_registry_enabled: true) + end + end + end + + it_behaves_like 'returning package1' + end + end end context 'with a group deploy token' do diff --git a/spec/finders/packages/npm/packages_for_user_finder_spec.rb b/spec/finders/packages/npm/packages_for_user_finder_spec.rb index e2dc21e1008..ffbb4f9e484 100644 --- a/spec/finders/packages/npm/packages_for_user_finder_spec.rb +++ b/spec/finders/packages/npm/packages_for_user_finder_spec.rb @@ -36,6 +36,24 @@ RSpec.describe ::Packages::Npm::PackagesForUserFinder, feature_category: :packag end it_behaves_like 'searches for packages' + + context 'when an user is a reporter of both projects' do + before_all do + project2.add_reporter(user) + end + + it { is_expected.to contain_exactly(package, package_with_diff_project) } + + context 'when the second project has the package registry disabled' do + before_all do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC, + package_registry_access_level: 'disabled', packages_enabled: false) + end + + it_behaves_like 'searches for packages' + end + end end end end diff --git a/spec/finders/projects/ml/model_finder_spec.rb b/spec/finders/projects/ml/model_finder_spec.rb index 48333ae49e5..1d869e1792d 100644 --- a/spec/finders/projects/ml/model_finder_spec.rb +++ b/spec/finders/projects/ml/model_finder_spec.rb @@ -22,4 +22,8 @@ RSpec.describe Projects::Ml::ModelFinder, feature_category: :mlops do it 'does not return models belonging to a different project' do is_expected.not_to include(model3) end + + it 'includes version count' do + expect(models[0].version_count).to be(models[0].versions.count) + end end diff --git a/spec/frontend/crm/crm_form_spec.js b/spec/frontend/crm/crm_form_spec.js index fabf43ceb9d..083b49b7c30 100644 --- a/spec/frontend/crm/crm_form_spec.js +++ b/spec/frontend/crm/crm_form_spec.js @@ -10,7 +10,7 @@ import routes from '~/crm/contacts/routes'; import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql'; import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql'; import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql'; -import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql'; +import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql'; import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql'; import { createContactMutationErrorResponse, diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js index 8408c1920a9..f15fcac71d5 100644 --- a/spec/frontend/crm/organization_form_wrapper_spec.js +++ b/spec/frontend/crm/organization_form_wrapper_spec.js @@ -2,7 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue'; import CrmForm from '~/crm/components/crm_form.vue'; import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql'; -import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql'; +import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql'; import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql'; describe('Customer relations organization form wrapper', () => { diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js index 9788b9062d6..ed15c66f4c6 100644 --- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js +++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js @@ -97,7 +97,7 @@ describe('~/frontend/environments/graphql/resolvers', () => { it('should request namespaced services from the cluster_client library if namespace is specified', async () => { const services = await mockResolvers.Query.k8sServices(null, { configuration, namespace }); - expect(mockNamespacedServicesListFn).toHaveBeenCalledWith(namespace); + expect(mockNamespacedServicesListFn).toHaveBeenCalledWith({ namespace }); expect(mockAllServicesListFn).not.toHaveBeenCalled(); expect(services).toEqual(k8sServicesMock); diff --git a/spec/frontend/import/details/mock_data.js b/spec/frontend/import/details/mock_data.js index 67148173404..b61a7f36f85 100644 --- a/spec/frontend/import/details/mock_data.js +++ b/spec/frontend/import/details/mock_data.js @@ -7,7 +7,7 @@ export const mockImportFailures = [ exception_class: 'ActiveRecord::RecordInvalid', exception_message: 'Record invalid', source: 'Gitlab::GithubImport::Importer::PullRequestImporter', - github_identifiers: { + external_identifiers: { iid: 2, issuable_type: 'MergeRequest', object_type: 'pull_request', @@ -22,7 +22,7 @@ export const mockImportFailures = [ exception_class: 'ActiveRecord::RecordInvalid', exception_message: 'Record invalid', source: 'Gitlab::GithubImport::Importer::PullRequestImporter', - github_identifiers: { + external_identifiers: { iid: 3, issuable_type: 'MergeRequest', object_type: 'pull_request', @@ -37,7 +37,7 @@ export const mockImportFailures = [ exception_class: 'NameError', exception_message: 'some message', source: 'Gitlab::GithubImport::Importer::LfsObjectImporter', - github_identifiers: { + external_identifiers: { oid: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb', size: 2473979, }, diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js deleted file mode 100644 index 14f39a35387..00000000000 --- a/spec/frontend/import_entities/components/group_dropdown_spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import GroupDropdown from '~/import_entities/components/group_dropdown.vue'; -import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql'; - -Vue.use(VueApollo); - -const makeGroupMock = (fullPath) => ({ - id: `gid://gitlab/Group/${fullPath}`, - fullPath, - name: fullPath, - visibility: 'public', - webUrl: `http://gdk.test:3000/groups/${fullPath}`, - __typename: 'Group', -}); - -const AVAILABLE_NAMESPACES = [ - makeGroupMock('match1'), - makeGroupMock('unrelated'), - makeGroupMock('match2'), -]; - -const SEARCH_NAMESPACES_MOCK = Promise.resolve({ - data: { - currentUser: { - id: 'gid://gitlab/User/1', - groups: { - nodes: AVAILABLE_NAMESPACES, - __typename: 'GroupConnection', - }, - namespace: { - id: 'gid://gitlab/Namespaces::UserNamespace/1', - fullPath: 'root', - __typename: 'Namespace', - }, - __typename: 'UserCore', - }, - }, -}); - -describe('Import entities group dropdown component', () => { - let wrapper; - let namespacesTracker; - - const createComponent = (propsData) => { - const apolloProvider = createMockApollo([ - [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK], - ]); - - namespacesTracker = jest.fn(); - - wrapper = shallowMount(GroupDropdown, { - apolloProvider, - scopedSlots: { - default: namespacesTracker, - }, - stubs: { GlDropdown }, - propsData, - }); - }; - - it('passes namespaces from graphql query to default slot', async () => { - createComponent(); - jest.advanceTimersByTime(DEBOUNCE_DELAY); - await nextTick(); - await waitForPromises(); - await nextTick(); - - expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: AVAILABLE_NAMESPACES }); - }); - - it('filters namespaces based on user input', async () => { - createComponent(); - - namespacesTracker.mockReset(); - wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match'); - jest.advanceTimersByTime(DEBOUNCE_DELAY); - await nextTick(); - await waitForPromises(); - await nextTick(); - - expect(namespacesTracker).toHaveBeenCalledWith({ - namespaces: [ - expect.objectContaining({ fullPath: 'match1' }), - expect.objectContaining({ fullPath: 'match2' }), - ], - }); - }); -}); diff --git a/spec/frontend/import_entities/components/import_target_dropdown_spec.js b/spec/frontend/import_entities/components/import_target_dropdown_spec.js index c12baed2374..ba0bb0b0f74 100644 --- a/spec/frontend/import_entities/components/import_target_dropdown_spec.js +++ b/spec/frontend/import_entities/components/import_target_dropdown_spec.js @@ -18,7 +18,6 @@ describe('ImportTargetDropdown', () => { const defaultProps = { selected: mockUserNamespace, - userNamespace: mockUserNamespace, }; const createComponent = ({ props = {} } = {}) => { @@ -39,7 +38,7 @@ describe('ImportTargetDropdown', () => { }; const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); - const findListboxUsersItems = () => findListbox().props('items')[0].options; + const findListboxFirstGroupItems = () => findListbox().props('items')[0].options; const findListboxGroupsItems = () => findListbox().props('items')[1].options; const waitForQuery = async () => { @@ -63,12 +62,54 @@ describe('ImportTargetDropdown', () => { expect(findListbox().props('toggleText')).toBe('a-group-path-that-is-lo…'); }); - it('passes userNamespace as "Users" group item', () => { - createComponent(); + describe('when used on group import', () => { + beforeEach(() => { + createComponent(); + }); - expect(findListboxUsersItems()).toEqual([ - { text: mockUserNamespace, value: mockUserNamespace }, - ]); + it('adds "No parent" in "Parent" group', () => { + expect(findListboxFirstGroupItems()).toEqual([{ text: 'No parent', value: '' }]); + }); + + it('emits "select" event with { fullPath: "", id: null } when "No parent" is selected', () => { + findListbox().vm.$emit('select', ''); + + expect(wrapper.emitted('select')[0]).toEqual([{ fullPath: '', id: null }]); + }); + + it('emits "select" event with { fullPath, id } when a group is selected', async () => { + await waitForQuery(); + + const mockGroupPath = 'match1'; + + findListbox().vm.$emit('select', mockGroupPath); + + expect(wrapper.emitted('select')[0]).toEqual([ + { fullPath: mockGroupPath, id: `gid://gitlab/Group/${mockGroupPath}` }, + ]); + }); + }); + + describe('when used on project import', () => { + beforeEach(() => { + createComponent({ + props: { userNamespace: mockUserNamespace }, + }); + }); + + it('passes userNamespace as "Users" group item', () => { + expect(findListboxFirstGroupItems()).toEqual([ + { text: mockUserNamespace, value: mockUserNamespace }, + ]); + }); + + it('emits "select" event with path as value', () => { + const mockProjectPath = 'mock-project'; + + findListbox().vm.$emit('select', mockProjectPath); + + expect(wrapper.emitted('select')[0]).toEqual([mockProjectPath]); + }); }); it('passes namespaces from GraphQL as "Groups" group item', async () => { diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 68fbd8dae18..4fab22e316a 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -57,7 +57,7 @@ describe('import table', () => { ]; const findPaginationDropdown = () => wrapper.findByTestId('page-size'); const findTargetNamespaceDropdown = (rowWrapper) => - extendedWrapper(rowWrapper).findByTestId('target-namespace-selector'); + extendedWrapper(rowWrapper).findByTestId('target-namespace-dropdown'); const findTargetNamespaceInput = (rowWrapper) => extendedWrapper(rowWrapper).findByTestId('target-namespace-input'); const findPaginationDropdownText = () => findPaginationDropdown().find('button').text(); diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js index 46884a42707..ac95026a9a4 100644 --- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js @@ -1,10 +1,9 @@ -import { GlDropdownItem, GlFormInput } from '@gitlab/ui'; +import { GlFormInput } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue'; +import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue'; import { STATUSES } from '~/import_entities/constants'; import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -37,7 +36,7 @@ describe('import target cell', () => { let group; const findNameInput = () => wrapper.findComponent(GlFormInput); - const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown); + const findNamespaceDropdown = () => wrapper.findComponent(ImportTargetDropdown); const createComponent = (props) => { apolloProvider = createMockApollo([ @@ -49,7 +48,7 @@ describe('import target cell', () => { wrapper = shallowMount(ImportTargetCell, { apolloProvider, - stubs: { ImportGroupDropdown }, + stubs: { ImportTargetDropdown }, propsData: { groupPathRegex: /.*/, ...props, @@ -73,14 +72,14 @@ describe('import target cell', () => { }); it('emits update-target-namespace when dropdown option is clicked', () => { - const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2); + const targetNamespace = { + fullPath: AVAILABLE_NAMESPACES[1].fullPath, + id: AVAILABLE_NAMESPACES[1].id, + }; - dropdownItem.vm.$emit('click'); + findNamespaceDropdown().vm.$emit('select', targetNamespace); - expect(wrapper.emitted('update-target-namespace')).toBeDefined(); - expect(wrapper.emitted('update-target-namespace')[0][0]).toStrictEqual( - AVAILABLE_NAMESPACES[1], - ); + expect(wrapper.emitted('update-target-namespace')[0]).toStrictEqual([targetNamespace]); }); }); @@ -101,36 +100,6 @@ describe('import target cell', () => { }); }); - it('renders only no parent option if available namespaces list is empty', () => { - createComponent({ - group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }), - availableNamespaces: [], - }); - - const items = findNamespaceDropdown() - .findAllComponents(GlDropdownItem) - .wrappers.map((w) => w.text()); - - expect(items[0]).toBe('No parent'); - expect(items).toHaveLength(1); - }); - - it('renders both no parent option and available namespaces list when available namespaces list is not empty', async () => { - createComponent({ - group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }), - }); - jest.advanceTimersByTime(DEBOUNCE_DELAY); - await waitForPromises(); - await nextTick(); - - const [firstItem, ...rest] = findNamespaceDropdown() - .findAllComponents(GlDropdownItem) - .wrappers.map((w) => w.text()); - - expect(firstItem).toBe('No parent'); - expect(rest).toHaveLength(AVAILABLE_NAMESPACES.length); - }); - describe('when entity is not available for import', () => { beforeEach(() => { group = generateFakeTableEntry({ @@ -147,6 +116,7 @@ describe('import target cell', () => { describe('when entity is available for import', () => { const FAKE_PROGRESS_MESSAGE = 'progress message'; + beforeEach(() => { group = generateFakeTableEntry({ id: 1, diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js index b4aad5ca292..c1b9aef9634 100644 --- a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js +++ b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js @@ -1,6 +1,6 @@ -import { GlLink } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import MlModelsIndexApp from '~/ml/model_registry/routes/models/index'; +import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue'; import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations'; import Pagination from '~/vue_shared/components/incubation/pagination.vue'; import { mockModels, startCursor, defaultPageInfo } from './mock_data'; @@ -10,10 +10,8 @@ const createWrapper = (propsData = { models: mockModels, pageInfo: defaultPageIn wrapper = shallowMountExtended(MlModelsIndexApp, { propsData }); }; -const findModelLink = (index) => wrapper.findAllComponents(GlLink).at(index); +const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index); const findPagination = () => wrapper.findComponent(Pagination); -const modelLinkText = (index) => findModelLink(index).text(); -const modelLinkHref = (index) => findModelLink(index).attributes('href'); const findTitle = () => wrapper.findByText(TITLE_LABEL); const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL); @@ -47,11 +45,8 @@ describe('MlModelsIndex', () => { describe('model list', () => { it('displays the models', () => { - expect(modelLinkHref(0)).toBe(mockModels[0].path); - expect(modelLinkText(0)).toBe(`${mockModels[0].name} / ${mockModels[0].version}`); - - expect(modelLinkHref(1)).toBe(mockModels[1].path); - expect(modelLinkText(1)).toBe(`${mockModels[1].name} / ${mockModels[1].version}`); + expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]); + expect(findModelRow(1).props('model')).toMatchObject(mockModels[1]); }); }); diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js index e65552353c3..841a543606f 100644 --- a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js +++ b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js @@ -3,14 +3,22 @@ export const mockModels = [ name: 'model_1', version: '1.0', path: 'path/to/model_1', + versionCount: 3, }, { name: 'model_2', - version: '1.0', + version: '1.1', path: 'path/to/model_2', + versionCount: 1, }, ]; +export const modelWithoutVersion = { + name: 'model_without_version', + path: 'path/to/model_without_version', + versionCount: 0, +}; + export const startCursor = 'eyJpZCI6IjE2In0'; export const defaultPageInfo = Object.freeze({ diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js new file mode 100644 index 00000000000..7600288f560 --- /dev/null +++ b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js @@ -0,0 +1,42 @@ +import { GlLink } from '@gitlab/ui'; +import { + mockModels, + modelWithoutVersion, +} from 'jest/ml/model_registry/routes/models/index/components/mock_data'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue'; + +let wrapper; +const createWrapper = (model = mockModels[0]) => { + wrapper = shallowMountExtended(ModelRow, { propsData: { model } }); +}; + +const findLink = () => wrapper.findComponent(GlLink); +const findMessage = (message) => wrapper.findByText(message); + +describe('ModelRow', () => { + beforeEach(() => { + createWrapper(); + }); + + it('Has a link to the model', () => { + expect(findLink().text()).toBe(mockModels[0].name); + expect(findLink().attributes('href')).toBe(mockModels[0].path); + }); + + it('Shows the latest version and the version count', () => { + expect(findMessage('1.0 · 3 versions').exists()).toBe(true); + }); + + it('Shows the latest version and no version count if it has only 1 version', () => { + createWrapper(mockModels[1]); + + expect(findMessage('1.1 · No other versions').exists()).toBe(true); + }); + + it('Shows no version message if model has no versions', () => { + createWrapper(modelWithoutVersion); + + expect(findMessage('No registered versions').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js index 34b7524d8fb..620c753e3c5 100644 --- a/spec/frontend/notes/components/email_participants_warning_spec.js +++ b/spec/frontend/notes/components/email_participants_warning_spec.js @@ -1,10 +1,12 @@ import { mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; + import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue'; describe('Email Participants Warning Component', () => { let wrapper; - const findMoreButton = () => wrapper.find('button'); + const findMoreButton = () => wrapper.findComponent(GlButton); const createWrapper = (emails) => { wrapper = mount(EmailParticipantsWarning, { @@ -48,7 +50,7 @@ describe('Email Participants Warning Component', () => { describe('when more button clicked', () => { beforeEach(() => { - findMoreButton().trigger('click'); + findMoreButton().vm.$emit('click'); }); it('more button no longer exists', () => { diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js new file mode 100644 index 00000000000..2206a6f0384 --- /dev/null +++ b/spec/frontend/organizations/new/components/app_spec.js @@ -0,0 +1,103 @@ +import VueApollo from 'vue-apollo'; +import Vue, { nextTick } from 'vue'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import App from '~/organizations/new/components/app.vue'; +import resolvers from '~/organizations/shared/graphql/resolvers'; +import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { createOrganizationResponse } from '~/organizations/mock_data'; +import { createAlert } from '~/alert'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +Vue.use(VueApollo); +jest.useFakeTimers(); + +jest.mock('~/lib/utils/url_utility'); +jest.mock('~/alert'); + +describe('OrganizationNewApp', () => { + let wrapper; + let mockApollo; + + const createComponent = ({ mockResolvers = resolvers } = {}) => { + mockApollo = createMockApollo([], mockResolvers); + + wrapper = shallowMountExtended(App, { apolloProvider: mockApollo }); + }; + + const findForm = () => wrapper.findComponent(NewEditForm); + const submitForm = async () => { + findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' }); + await nextTick(); + }; + + afterEach(() => { + mockApollo = null; + }); + + it('renders form', () => { + createComponent(); + + expect(findForm().exists()).toBe(true); + }); + + describe('when form is submitted', () => { + describe('when API is loading', () => { + beforeEach(async () => { + const mockResolvers = { + Mutation: { + createOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})), + }, + }; + + createComponent({ mockResolvers }); + + await submitForm(); + }); + + it('sets `NewEditForm` `loading` prop to `true`', () => { + expect(findForm().props('loading')).toBe(true); + }); + }); + + describe('when API request is successful', () => { + beforeEach(async () => { + createComponent(); + await submitForm(); + jest.runAllTimers(); + await waitForPromises(); + }); + + it('redirects user to organization path', () => { + expect(visitUrl).toHaveBeenCalledWith(createOrganizationResponse.organization.path); + }); + }); + + describe('when API request is not successful', () => { + const error = new Error(); + + beforeEach(async () => { + const mockResolvers = { + Mutation: { + createOrganization: jest.fn().mockRejectedValueOnce(error), + }, + }; + + createComponent({ mockResolvers }); + await submitForm(); + jest.runAllTimers(); + await waitForPromises(); + }); + + it('displays error alert', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred creating an organization. Please try again.', + error, + captureError: true, + }); + }); + }); + }); +}); diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js new file mode 100644 index 00000000000..43c099fbb1c --- /dev/null +++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js @@ -0,0 +1,112 @@ +import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui'; + +import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +describe('NewEditForm', () => { + let wrapper; + + const defaultProvide = { + organizationsPath: '/-/organizations', + rootUrl: 'http://127.0.0.1:3000/', + }; + + const defaultPropsData = { + loading: false, + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = mountExtended(NewEditForm, { + attachTo: document.body, + provide: defaultProvide, + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + }; + + const findNameField = () => wrapper.findByLabelText('Organization name'); + const findUrlField = () => wrapper.findByLabelText('Organization URL'); + const submitForm = async () => { + await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click'); + }; + + it('renders `Organization name` field', () => { + createComponent(); + + expect(findNameField().exists()).toBe(true); + }); + + it('renders `Organization URL` field', () => { + createComponent(); + + expect(wrapper.findComponent(GlInputGroupText).findComponent(GlTruncate).props('text')).toBe( + 'http://127.0.0.1:3000/-/organizations/', + ); + expect(findUrlField().exists()).toBe(true); + }); + + describe('when form is submitted without filling in required fields', () => { + beforeEach(async () => { + createComponent(); + await submitForm(); + }); + + it('shows error messages', () => { + expect(wrapper.findByText('Organization name is required.').exists()).toBe(true); + expect(wrapper.findByText('Organization URL is required.').exists()).toBe(true); + }); + }); + + describe('when form is submitted successfully', () => { + beforeEach(async () => { + createComponent(); + + await findNameField().setValue('Foo bar'); + await findUrlField().setValue('foo-bar'); + await submitForm(); + }); + + it('emits `submit` event with form values', () => { + expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]); + }); + }); + + describe('when `Organization URL` has not been manually set', () => { + beforeEach(async () => { + createComponent(); + + await findNameField().setValue('Foo bar'); + await submitForm(); + }); + + it('sets `Organization URL` when typing in `Organization name`', () => { + expect(findUrlField().element.value).toBe('foo-bar'); + }); + }); + + describe('when `Organization URL` has been manually set', () => { + beforeEach(async () => { + createComponent(); + + await findUrlField().setValue('foo-bar-baz'); + await findNameField().setValue('Foo bar'); + await submitForm(); + }); + + it('does not modify `Organization URL` when typing in `Organization name`', () => { + expect(findUrlField().element.value).toBe('foo-bar-baz'); + }); + }); + + describe('when `loading` prop is `true`', () => { + beforeEach(() => { + createComponent({ propsData: { loading: true } }); + }); + + it('shows button with loading icon', () => { + expect(wrapper.findComponent(GlButton).props('loading')).toBe(true); + }); + }); +}); diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 67aa57dcad7..cf8ae358e49 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -7,6 +7,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do let_it_be(:new_group_path) { '/groups/new' } let_it_be(:new_project_path) { '/projects/new' } let_it_be(:organizations_empty_state_svg_path) { 'illustrations/empty-state/empty-organizations-md.svg' } + let_it_be(:organizations_path) { '/-/organizations/' } + let_it_be(:root_url) { 'http://127.0.0.1:3000/' } let_it_be(:groups_empty_state_svg_path) { 'illustrations/empty-state/empty-groups-md.svg' } let_it_be(:projects_empty_state_svg_path) { 'illustrations/empty-state/empty-projects-md.svg' } @@ -15,6 +17,8 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do allow(helper).to receive(:new_project_path).and_return(new_project_path) allow(helper).to receive(:image_path).with(organizations_empty_state_svg_path) .and_return(organizations_empty_state_svg_path) + allow(helper).to receive(:organizations_path).and_return(organizations_path) + allow(helper).to receive(:root_url).and_return(root_url) allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path) allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path) end @@ -76,4 +80,15 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do ) end end + + describe '#organization_new_app_data' do + it 'returns expected json' do + expect(Gitlab::Json.parse(helper.organization_new_app_data)).to eq( + { + 'organizations_path' => organizations_path, + 'root_url' => root_url + } + ) + end + end end diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 97843781891..0bdcfcfd546 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_composition do let_it_be(:user) { create(:user) } - let(:path) { described_class.new(address: address, content_filename: 'template.yml') } + let(:path) { described_class.new(address: address) } let(:settings) { GitlabSettings::Options.build({ 'component_fqdn' => current_host }) } let(:current_host) { 'acme.com/' } @@ -44,9 +44,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline context 'when the component is simple (single file template)' do it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_1') + expect(result.path).to eq('templates/secret-detection.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/secret-detection.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -56,9 +57,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/#{project_path}/dast@#{version}" } it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_2') + expect(result.path).to eq('templates/dast/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/dast/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -67,7 +69,8 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/#{project_path}/dast/another-folder@#{version}" } it 'returns nil' do - expect(path.fetch_content!(current_user: user)).to be_nil + result = path.fetch_content!(current_user: user) + expect(result.content).to be_nil end end @@ -75,7 +78,8 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/#{project_path}/dast/another-template@#{version}" } it 'returns nil' do - expect(path.fetch_content!(current_user: user)).to be_nil + result = path.fetch_content!(current_user: user) + expect(result.content).to be_nil end end end @@ -110,9 +114,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_2') + expect(result.path).to eq('templates/secret-detection.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/secret-detection.yml') expect(path.project).to eq(project) expect(path.sha).to eq(latest_sha) end @@ -124,7 +129,6 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline it 'returns nil', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to be_nil expect(path.host).to eq(current_host) - expect(path.project_file_path).to be_nil expect(path.project).to eq(project) expect(path.sha).to be_nil end @@ -135,9 +139,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:current_host) { 'acme.com/gitlab/' } it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_1') + expect(result.path).to eq('templates/secret-detection.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('templates/secret-detection.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -164,9 +169,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine') + expect(result.path).to eq('component/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -184,9 +190,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine') + expect(result.path).to eq('component/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -197,9 +204,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:current_host) { 'acme.com/gitlab/' } it 'fetches the component content', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine') + expect(result.path).to eq('component/template.yml') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') expect(path.project).to eq(project) expect(path.sha).to eq(project.commit('master').id) end @@ -211,7 +219,6 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline it 'returns nil', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to be_nil expect(path.host).to eq(current_host) - expect(path.project_file_path).to be_nil expect(path.project).to eq(project) expect(path.sha).to be_nil end diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index 0f7b811b5df..88e272ac3fd 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -99,7 +99,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: let(:response) do ServiceResponse.success(payload: { content: content, - path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + path: 'templates/component.yml', + project: project, + sha: '12345' }) end @@ -132,7 +134,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: let(:response) do ServiceResponse.success(payload: { content: content, - path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + path: 'templates/component.yml', + project: project, + sha: '12345' }) end @@ -158,15 +162,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: describe '#metadata' do subject(:metadata) { external_resource.metadata } - let(:component_path) do - instance_double(::Gitlab::Ci::Components::InstancePath, - project: project, - sha: '12345', - project_file_path: 'my-component/template.yml') - end - let(:response) do - ServiceResponse.success(payload: { path: component_path }) + ServiceResponse.success(payload: { path: 'my-component/template.yml', project: project, sha: '12345' }) end it 'returns the metadata' do @@ -183,14 +180,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: end describe '#expand_context' do - let(:component_path) do - instance_double(::Gitlab::Ci::Components::InstancePath, - project: project, - sha: '12345') - end - let(:response) do - ServiceResponse.success(payload: { path: component_path }) + ServiceResponse.success(payload: { path: 'templates/component.yml', project: project, sha: '12345' }) end subject { external_resource.send(:expand_context_attrs) } @@ -207,11 +198,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: describe '#to_hash' do context 'when interpolation is being used' do let(:response) do - ServiceResponse.success(payload: { content: content, path: path }) - end - - let(:path) do - instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + ServiceResponse.success(payload: { content: content, path: 'templates/component.yml', project: project, + sha: '12345' }) end let(:content) do diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb index 57a9a47d699..684da1df43b 100644 --- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb @@ -58,4 +58,36 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c end end end + + describe '#load_uninterpolated_yaml' do + let(:yaml) do + <<~YAML + --- + spec: + inputs: + test_input: + --- + test_job: + script: + - echo "$[[ inputs.test_input ]]" + YAML + end + + subject(:result) { described_class.new(yaml).load_uninterpolated_yaml } + + it 'returns the config' do + expected_content = { test_job: { script: ["echo \"$[[ inputs.test_input ]]\""] } } + expect(result).to be_valid + expect(result.content).to eq(expected_content) + end + + context 'when there is a format error in the yaml' do + let(:yaml) { 'invalid: yaml: all the time' } + + it 'returns an error' do + expect(result).not_to be_valid + expect(result.error).to include('mapping values are not allowed in this context') + end + end + end end diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb index a66c630dfc9..5e9dee02190 100644 --- a/spec/lib/gitlab/ci/config/yaml/result_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb @@ -3,12 +3,44 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_composition do + it 'raises an error when reading a header when there is none' do + result = described_class.new(config: { b: 2 }) + + expect { result.header }.to raise_error(ArgumentError) + end + + it 'stores an error / exception when initialized with it' do + result = described_class.new(error: ArgumentError.new('abc')) + + expect(result).not_to be_valid + expect(result.error).to be_a ArgumentError + end + it 'does not have a header when config is a single hash' do result = described_class.new(config: { a: 1, b: 2 }) expect(result).not_to have_header end + describe '#inputs' do + it 'returns the value of the spec inputs' do + result = described_class.new(config: [{ spec: { inputs: { website: nil } } }, { b: 2 }]) + + expect(result).to have_header + expect(result.inputs).to eq({ website: nil }) + end + end + + describe '#interpolated?' do + it 'defaults to false' do + expect(described_class.new).not_to be_interpolated + end + + it 'returns the value passed to the initializer' do + expect(described_class.new(interpolated: true)).to be_interpolated + end + end + context 'when config is an array of hashes' do context 'when first document matches the header schema' do it 'has a header' do @@ -38,27 +70,4 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_com expect(result.content).to be_empty end end - - it 'raises an error when reading a header when there is none' do - result = described_class.new(config: { b: 2 }) - - expect { result.header }.to raise_error(ArgumentError) - end - - it 'stores an error / exception when initialized with it' do - result = described_class.new(error: ArgumentError.new('abc')) - - expect(result).not_to be_valid - expect(result.error).to be_a ArgumentError - end - - describe '#interpolated?' do - it 'defaults to false' do - expect(described_class.new).not_to be_interpolated - end - - it 'returns the value passed to the initializer' do - expect(described_class.new(interpolated: true)).to be_interpolated - end - end end diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb index 2c57106b07c..dacbe07c8b3 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb @@ -42,16 +42,15 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category: it { is_expected.to be_nil } end - context 'when no dependency_scanning or container_scanning properties are present' do + context 'when no dependency_scanning properties are present' do let(:properties) do [ { 'name' => 'gitlab:meta:schema_version', 'value' => '1' } ] end - it 'does not call source parsers' do + it 'does not call dependency_scanning parser' do expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:source) - expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).not_to receive(:source) parse_source_from_properties end @@ -86,35 +85,4 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties, feature_category: parse_source_from_properties end end - - context 'when container_scanning properties are present' do - let(:properties) do - [ - { 'name' => 'gitlab:meta:schema_version', 'value' => '1' }, - { 'name' => 'gitlab:container_scanning:image:name', 'value' => 'photon' }, - { 'name' => 'gitlab:container_scanning:image:tag', 'value' => '5.0-20231007' }, - { 'name' => 'gitlab:container_scanning:operating_system:name', 'value' => 'Photon OS' }, - { 'name' => 'gitlab:container_scanning:operating_system:version', 'value' => '5.0' } - ] - end - - let(:expected_input) do - { - 'image' => { - 'name' => 'photon', - 'tag' => '5.0-20231007' - }, - 'operating_system' => { - 'name' => 'Photon OS', - 'version' => '5.0' - } - } - end - - it 'passes only supported properties to the container scanning parser' do - expect(Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning).to receive(:source).with(expected_input) - - parse_source_from_properties - end - end end diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb deleted file mode 100644 index 1b4426c4e5a..00000000000 --- a/spec/lib/gitlab/ci/parsers/sbom/source/container_scanning_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' - -RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::ContainerScanning, feature_category: :container_scanning do - subject { described_class.source(property_data) } - - context 'when all property data is present' do - let(:property_data) do - { - 'image' => { - 'name' => 'photon', - 'tag' => '5.0-20231007' - }, - 'operating_system' => { - 'name' => 'Photon OS', - 'version' => '5.0' - } - } - end - - it 'returns expected source data' do - is_expected.to have_attributes( - source_type: :container_scanning, - data: property_data - ) - end - end - - context 'when required properties are missing' do - let(:property_data) do - { - 'image' => { - 'tag' => '5.0-20231007' - }, - 'operating_system' => { - 'name' => 'Photon OS', - 'version' => '5.0' - } - } - end - - it { is_expected.to be_nil } - end -end diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb index 28fbd4d883f..6b4984ceaf2 100644 --- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb +++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb @@ -47,10 +47,9 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers .with(object) .and_return(false) - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: '1 object_types fetched' @@ -82,10 +81,9 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers .with(object) .and_return(true) - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:info) .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: '0 object_types fetched' @@ -145,14 +143,13 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers } ) - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:error) .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: ['Title is invalid'], - github_identifiers: { id: 12345, title: 'bug,bug', object_type: :object_type } + external_identifiers: { id: 12345, title: 'bug,bug', object_type: :object_type } ) expect(Gitlab::GithubImport::ObjectCounter) @@ -172,7 +169,7 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers expect(errors).not_to be_empty expect(errors[0][:validation_errors].full_messages).to match_array(['Title is invalid']) - expect(errors[0][:github_identifiers]).to eq({ id: 12345, title: 'bug,bug', object_type: :object_type }) + expect(errors[0][:external_identifiers]).to eq({ id: 12345, title: 'bug,bug', object_type: :object_type }) end end end @@ -182,11 +179,10 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers it 'bulk inserts rows into the database' do rows = [{ title: 'Foo' }] * 10 - expect(Gitlab::Import::Logger) + expect(Gitlab::GithubImport::Logger) .to receive(:info) .twice .with( - import_type: :github, project_id: 1, importer: 'MyImporter', message: '5 object_types imported' @@ -243,7 +239,7 @@ RSpec.describe Gitlab::GithubImport::BulkImporting, feature_category: :importers importer.bulk_insert_failures([{ validation_errors: error, - github_identifiers: { id: 123456 } + external_identifiers: { id: 123456 } }]) end end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 4b0d61e3188..5f321a15de9 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -316,7 +316,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do allow_retry expect(client).to receive(:requests_remaining?).twice.and_return(true) - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once + expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once expect(client.with_rate_limit(&block_to_rate_limit)).to eq({}) end @@ -337,7 +337,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do it 'retries on error and succeeds' do allow_retry - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once + expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once expect(client.with_rate_limit(&block_to_rate_limit)).to eq({}) end @@ -723,7 +723,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do it 'retries on error and succeeds' do allow_retry(:post) - expect(Gitlab::Import::Logger).to receive(:info).with(hash_including(info_params)).once + expect(Gitlab::GithubImport::Logger).to receive(:info).with(hash_including(info_params)).once expect(client.search_repos_by_name_graphql('test')).to eq({}) end diff --git a/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb index 7890561bf2d..b44f1ec85f3 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/issues_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project issue attachments' do + expect(project.issues).to receive(:id_not_in).with([]).and_return(project.issues) expect(project.issues).to receive(:select).with(:id, :description, :iid).and_call_original expect_next_instances_of( @@ -32,6 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::IssuesImporter do it "doesn't import this issue attachments" do importer.mark_as_imported(issue_1) + expect(project.issues).to receive(:id_not_in).with([issue_1.id.to_s]).and_call_original expect_next_instance_of( Gitlab::GithubImport::Importer::NoteAttachmentsImporter, *importer_attrs ) do |note_attachments_importer| diff --git a/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb index e5aa17dd81e..381cb17bb52 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/merge_requests_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporte let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project merge request attachments' do + expect(project.merge_requests).to receive(:id_not_in).with([]).and_return(project.merge_requests) expect(project.merge_requests).to receive(:select).with(:id, :description, :iid).and_call_original expect_next_instances_of( @@ -32,6 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporte it "doesn't import this merge request attachments" do importer.mark_as_imported(merge_request_1) + expect(project.merge_requests).to receive(:id_not_in).with([merge_request_1.id.to_s]).and_call_original expect_next_instance_of( Gitlab::GithubImport::Importer::NoteAttachmentsImporter, *importer_attrs ) do |note_attachments_importer| diff --git a/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb index 7ed353e1b71..5b3ad032702 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/notes_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -18,6 +18,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project user note' do + expect(project.notes).to receive(:id_not_in).with([]).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) .with(*importer_attrs).twice.and_return(importer_stub) expect(importer_stub).to receive(:execute).twice @@ -29,6 +30,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::NotesImporter do it "doesn't import this note" do importer.mark_as_imported(note_1) + expect(project.notes).to receive(:id_not_in).with([note_1.id.to_s]).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) .with(*importer_attrs).once.and_return(importer_stub) expect(importer_stub).to receive(:execute).once diff --git a/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb index e1b009c3eeb..c1c19c40afb 100644 --- a/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/attachments/releases_importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do +RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter, feature_category: :importers do subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project) } @@ -17,6 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do let(:importer_attrs) { [instance_of(Gitlab::GithubImport::Representation::NoteText), project, client] } it 'imports each project release' do + expect(project.releases).to receive(:id_not_in).with([]).and_return(project.releases) expect(project.releases).to receive(:select).with(:id, :description, :tag).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) @@ -30,6 +31,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Attachments::ReleasesImporter do it "doesn't import this release" do importer.mark_as_imported(release_1) + expect(project.releases).to receive(:id_not_in).with([release_1.id.to_s]).and_call_original expect(Gitlab::GithubImport::Importer::NoteAttachmentsImporter).to receive(:new) .with(*importer_attrs).once.and_return(importer_stub) expect(importer_stub).to receive(:execute).once diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb index fc8d9cee066..0328a36b646 100644 --- a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb @@ -50,13 +50,12 @@ feature_category: :importers do label = { id: 1, name: 'bug,bug', color: 'ffffff' } expect(importer).to receive(:each_label).and_return([label]) - expect(Gitlab::Import::Logger).to receive(:error) + expect(Gitlab::GithubImport::Logger).to receive(:error) .with( - import_type: :github, project_id: project.id, importer: described_class.name, message: ['Title is invalid'], - github_identifiers: { title: 'bug,bug', object_type: :label } + external_identifiers: { title: 'bug,bug', object_type: :label } ) rows, errors = importer.build_labels diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb index cf44d510c80..fa7283d210b 100644 --- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb @@ -80,13 +80,12 @@ RSpec.describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab .to receive(:each_milestone) .and_return([milestone]) - expect(Gitlab::Import::Logger).to receive(:error) + expect(Gitlab::GithubImport::Logger).to receive(:error) .with( - import_type: :github, project_id: project.id, importer: described_class.name, message: ["Title can't be blank"], - github_identifiers: { iid: 2, object_type: :milestone, title: nil } + external_identifiers: { iid: 2, object_type: :milestone, title: nil } ) rows, errors = importer.build_milestones diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index eddde272d2c..cfd75fba849 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -149,7 +149,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter, feature_cat it 'updates the repository' do importer = described_class.new(project, client) - expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect_next_instance_of(Gitlab::GithubImport::Logger) do |logger| expect(logger) .to receive(:info) .with(an_instance_of(Hash)) diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb index a3d20af22c7..1cfbe8e20ae 100644 --- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb @@ -148,7 +148,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ReleasesImporter, feature_categor expect(errors[0][:validation_errors].full_messages).to match_array( ['Description is too long (maximum is 1000000 characters)'] ) - expect(errors[0][:github_identifiers]).to eq({ tag: '1.0', object_type: :release }) + expect(errors[0][:external_identifiers]).to eq({ tag: '1.0', object_type: :release }) end end diff --git a/spec/lib/gitlab/import_export/attributes_finder_spec.rb b/spec/lib/gitlab/import_export/attributes_finder_spec.rb index f12cbe4f82f..fd9d609992d 100644 --- a/spec/lib/gitlab/import_export/attributes_finder_spec.rb +++ b/spec/lib/gitlab/import_export/attributes_finder_spec.rb @@ -131,19 +131,19 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :import end it 'generates the correct hash for a relation with included attributes' do - setup_yaml(tree: { project: [:issues] }, - included_attributes: { issues: [:name, :description] }) + setup_yaml( + tree: { project: [:issues] }, + included_attributes: { issues: [:name, :description] } + ) is_expected.to match( - include: [{ issues: { include: [], - only: [:name, :description] } }], + include: [{ issues: { include: [], only: [:name, :description] } }], preload: { issues: nil } ) end it 'generates the correct hash for a relation with excluded attributes' do - setup_yaml(tree: { project: [:issues] }, - excluded_attributes: { issues: [:name] }) + setup_yaml(tree: { project: [:issues] }, excluded_attributes: { issues: [:name] }) is_expected.to match( include: [{ issues: { except: [:name], @@ -153,25 +153,23 @@ RSpec.describe Gitlab::ImportExport::AttributesFinder, feature_category: :import end it 'generates the correct hash for a relation with both excluded and included attributes' do - setup_yaml(tree: { project: [:issues] }, - excluded_attributes: { issues: [:name] }, - included_attributes: { issues: [:description] }) + setup_yaml( + tree: { project: [:issues] }, + excluded_attributes: { issues: [:name] }, + included_attributes: { issues: [:description] } + ) is_expected.to match( - include: [{ issues: { except: [:name], - include: [], - only: [:description] } }], + include: [{ issues: { except: [:name], include: [], only: [:description] } }], preload: { issues: nil } ) end it 'generates the correct hash for a relation with custom methods' do - setup_yaml(tree: { project: [:issues] }, - methods: { issues: [:name] }) + setup_yaml(tree: { project: [:issues] }, methods: { issues: [:name] }) is_expected.to match( - include: [{ issues: { include: [], - methods: [:name] } }], + include: [{ issues: { include: [], methods: [:name] } }], preload: { issues: nil } ) end diff --git a/spec/lib/gitlab/import_export/base/object_builder_spec.rb b/spec/lib/gitlab/import_export/base/object_builder_spec.rb index 38c3b23db36..3c69a6a7746 100644 --- a/spec/lib/gitlab/import_export/base/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/base/object_builder_spec.rb @@ -4,11 +4,13 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::Base::ObjectBuilder do let(:project) do - create(:project, :repository, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project') + create( + :project, :repository, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project' + ) end let(:klass) { Milestone } diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb index 4ef8f4b5d76..5e63804c51c 100644 --- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb @@ -11,14 +11,16 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do let(:excluded_keys) { [] } subject do - described_class.create(relation_sym: relation_sym, # rubocop:disable Rails/SaveBang - relation_hash: relation_hash, - relation_index: 1, - object_builder: Gitlab::ImportExport::Project::ObjectBuilder, - members_mapper: members_mapper, - user: user, - importable: project, - excluded_keys: excluded_keys) + described_class.create( # rubocop:disable Rails/SaveBang + relation_sym: relation_sym, + relation_hash: relation_hash, + relation_index: 1, + object_builder: Gitlab::ImportExport::Project::ObjectBuilder, + members_mapper: members_mapper, + user: user, + importable: project, + excluded_keys: excluded_keys + ) end describe '#create' do diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb index 5ef9eb78d3b..144617055ab 100644 --- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb @@ -12,9 +12,7 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(exportable: project_with_design_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) } let(:restorer) do - described_class.new(path_to_bundle: bundle_path, - shared: shared, - importable: project) + described_class.new(path_to_bundle: bundle_path, shared: shared, importable: project) end before do diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb index 02419267f0e..dfc7202194d 100644 --- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb @@ -217,17 +217,18 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_ release = create(:release) group = create(:group) - project = create(:project, - :public, - :repository, - :issues_disabled, - :wiki_enabled, - :builds_private, - description: 'description', - releases: [release], - group: group, - approvals_before_merge: 1 - ) + project = create( + :project, + :public, + :repository, + :issues_disabled, + :wiki_enabled, + :builds_private, + description: 'description', + releases: [release], + group: group, + approvals_before_merge: 1 + ) issue = create(:issue, assignees: [user], project: project) snippet = create(:project_snippet, project: project) @@ -249,10 +250,7 @@ RSpec.describe Gitlab::ImportExport::FastHashSerializer, :with_license, feature_ create(:discussion_note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) - create(:note_on_commit, - author: user, - project: project, - commit_id: ci_build.pipeline.sha) + create(:note_on_commit, author: user, project: project, commit_id: ci_build.pipeline.sha) create(:resource_label_event, label: project_label, issue: issue) create(:resource_label_event, label: group_label, merge_request: merge_request) diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 3ca9f727033..17d416b0f0a 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -16,10 +16,12 @@ RSpec.describe Gitlab::ImportExport::MergeRequestParser do let(:diff_head_sha) { SecureRandom.hex(20) } let(:parsed_merge_request) do - described_class.new(project, - diff_head_sha, - merge_request, - merge_request.as_json).parse! + described_class.new( + project, + diff_head_sha, + merge_request, + merge_request.as_json + ).parse! end after do diff --git a/spec/lib/gitlab/import_export/project/object_builder_spec.rb b/spec/lib/gitlab/import_export/project/object_builder_spec.rb index 43794ce01a3..20e176bf6fd 100644 --- a/spec/lib/gitlab/import_export/project/object_builder_spec.rb +++ b/spec/lib/gitlab/import_export/project/object_builder_spec.rb @@ -6,12 +6,15 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do let!(:group) { create(:group, :private) } let!(:subgroup) { create(:group, :private, parent: group) } let!(:project) do - create(:project, :repository, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: subgroup) + create( + :project, + :repository, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: subgroup + ) end let(:lru_cache) { subject.send(:lru_cache) } @@ -19,10 +22,7 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do context 'request store is not active' do subject do - described_class.new(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group) + described_class.new(Label, 'title' => 'group label', 'project' => project, 'group' => project.group) end it 'ignore cache initialize' do @@ -33,10 +33,7 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do context 'request store is active', :request_store do subject do - described_class.new(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group) + described_class.new(Label, 'title' => 'group label', 'project' => project, 'group' => project.group) end it 'initialize cache in memory' do @@ -71,27 +68,33 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do it 'finds the existing group label' do group_label = create(:group_label, name: 'group label', group: project.group) - expect(described_class.build(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group)).to eq(group_label) + expect(described_class.build( + Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group + )).to eq(group_label) end it 'finds the existing group label in root ancestor' do group_label = create(:group_label, name: 'group label', group: group) - expect(described_class.build(Label, - 'title' => 'group label', - 'project' => project, - 'group' => group)).to eq(group_label) + expect(described_class.build( + Label, + 'title' => 'group label', + 'project' => project, + 'group' => group + )).to eq(group_label) end it 'creates a new project label' do - label = described_class.build(Label, - 'title' => 'group label', - 'project' => project, - 'group' => project.group, - 'group_id' => project.group.id) + label = described_class.build( + Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group, + 'group_id' => project.group.id + ) expect(label.persisted?).to be true expect(label).to be_an_instance_of(ProjectLabel) @@ -103,26 +106,32 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do it 'finds the existing group milestone' do milestone = create(:milestone, name: 'group milestone', group: project.group) - expect(described_class.build(Milestone, - 'title' => 'group milestone', - 'project' => project, - 'group' => project.group)).to eq(milestone) + expect(described_class.build( + Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group + )).to eq(milestone) end it 'finds the existing group milestone in root ancestor' do milestone = create(:milestone, name: 'group milestone', group: group) - expect(described_class.build(Milestone, - 'title' => 'group milestone', - 'project' => project, - 'group' => group)).to eq(milestone) + expect(described_class.build( + Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => group + )).to eq(milestone) end it 'creates a new milestone' do - milestone = described_class.build(Milestone, - 'title' => 'group milestone', - 'project' => project, - 'group' => project.group) + milestone = described_class.build( + Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group + ) expect(milestone.persisted?).to be true end @@ -132,12 +141,14 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do clashing_iid = 1 create(:milestone, iid: clashing_iid, project: project) - milestone = described_class.build(Milestone, - 'iid' => clashing_iid, - 'title' => 'milestone', - 'project' => project, - 'group' => nil, - 'group_id' => nil) + milestone = described_class.build( + Milestone, + 'iid' => clashing_iid, + 'title' => 'milestone', + 'project' => project, + 'group' => nil, + 'group_id' => nil + ) expect(milestone.persisted?).to be true expect(Milestone.count).to eq(2) @@ -173,34 +184,45 @@ RSpec.describe Gitlab::ImportExport::Project::ObjectBuilder do context 'merge_request' do it 'finds the existing merge_request' do - merge_request = create(:merge_request, title: 'MergeRequest', iid: 7, target_project: project, source_project: project) - expect(described_class.build(MergeRequest, - 'title' => 'MergeRequest', - 'source_project_id' => project.id, - 'target_project_id' => project.id, - 'source_branch' => 'SourceBranch', - 'iid' => 7, - 'target_branch' => 'TargetBranch', - 'author_id' => project.creator.id)).to eq(merge_request) + merge_request = create( + :merge_request, + title: 'MergeRequest', + iid: 7, + target_project: project, + source_project: project + ) + + expect(described_class.build( + MergeRequest, + 'title' => 'MergeRequest', + 'source_project_id' => project.id, + 'target_project_id' => project.id, + 'source_branch' => 'SourceBranch', + 'iid' => 7, + 'target_branch' => 'TargetBranch', + 'author_id' => project.creator.id + )).to eq(merge_request) end it 'creates a new merge_request' do - merge_request = described_class.build(MergeRequest, - 'title' => 'MergeRequest', - 'iid' => 8, - 'source_project_id' => project.id, - 'target_project_id' => project.id, - 'source_branch' => 'SourceBranch', - 'target_branch' => 'TargetBranch', - 'author_id' => project.creator.id) + merge_request = described_class.build( + MergeRequest, + 'title' => 'MergeRequest', + 'iid' => 8, + 'source_project_id' => project.id, + 'target_project_id' => project.id, + 'source_branch' => 'SourceBranch', + 'target_branch' => 'TargetBranch', + 'author_id' => project.creator.id + ) + expect(merge_request.persisted?).to be true end end context 'merge request diff commit users' do it 'finds the existing user' do - user = MergeRequest::DiffCommitUser - .find_or_create('Alice', 'alice@example.com') + user = MergeRequest::DiffCommitUser.find_or_create('Alice', 'alice@example.com') found = described_class.build( MergeRequest::DiffCommitUser, diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index c214f36046c..14af3028a6e 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -854,12 +854,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i end let!(:project) do - create(:project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: group) + create( + :project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: group + ) end before do @@ -890,12 +892,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i context 'with existing group models' do let(:group) { create(:group).tap { |g| g.add_maintainer(user) } } let!(:project) do - create(:project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: group) + create( + :project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: group + ) end before do @@ -926,12 +930,14 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i context 'with clashing milestones on IID' do let(:group) { create(:group).tap { |g| g.add_maintainer(user) } } let!(:project) do - create(:project, - :builds_disabled, - :issues_disabled, - name: 'project', - path: 'project', - group: group) + create( + :project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: group + ) end before do @@ -1143,8 +1149,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') } let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') } let_it_be(:project) do - create(:project, :builds_disabled, :issues_disabled, - { name: 'project', path: 'project' }) + create(:project, :builds_disabled, :issues_disabled, { name: 'project', path: 'project' }) end let(:shared) { project.import_export_shared } diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index 408ed3a2176..37a59a68188 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -74,12 +74,12 @@ RSpec.describe Gitlab::ImportExport::Shared do expect(Gitlab::ErrorTracking) .to receive(:track_exception) .with(error, hash_including( - importer: 'Import/Export', - project_id: project.id, - project_name: project.name, - project_path: project.full_path, - import_jid: import_state.jid - )) + importer: 'Import/Export', + project_id: project.id, + project_name: project.name, + project_path: project.full_path, + import_jid: import_state.jid + )) subject.error(error) end diff --git a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb index 2f39cb560d0..d7b1b180e2e 100644 --- a/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb @@ -10,10 +10,12 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do let(:shared) { project.import_export_shared } let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) } let(:restorer) do - described_class.new(user: user, - shared: shared, - snippet: snippet, - path_to_bundle: snippet_bundle_path) + described_class.new( + user: user, + shared: shared, + snippet: snippet, + path_to_bundle: snippet_bundle_path + ) end after do diff --git a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb index e348e8f7991..4a9a01475cb 100644 --- a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb @@ -14,9 +14,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer, :clean_gitlab_redis_r let(:bundle_dir) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) } let(:service) { instance_double(Gitlab::ImportExport::SnippetRepoRestorer) } let(:restorer) do - described_class.new(user: user, - shared: shared, - project: project) + described_class.new(user: user, shared: shared, project: project) end after do diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb new file mode 100644 index 00000000000..d7e0ee2079c --- /dev/null +++ b/spec/models/ci/catalog/components_project_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_composition do + using RSpec::Parameterized::TableSyntax + + let_it_be(:files) do + { + 'templates/secret-detection.yml' => "spec:\n inputs:\n website:\n---\nimage: alpine_1", + 'templates/dast/template.yml' => 'image: alpine_2', + 'templates/template.yml' => 'image: alpine_3', + 'templates/blank-yaml.yml' => '', + 'templates/dast/sub-folder/template.yml' => 'image: alpine_4', + 'tests/test.yml' => 'image: alpine_5', + 'README.md' => 'Read me' + } + end + + let_it_be(:project) do + create( + :project, :custom_repo, + description: 'Simple, complex, and other components', + files: files + ) + end + + let_it_be(:catalog_resource) { create(:ci_catalog_resource, project: project) } + + let(:components_project) { described_class.new(project, project.default_branch) } + + describe '#fetch_component_paths' do + it 'retrieves all the paths for valid components' do + paths = components_project.fetch_component_paths(project.default_branch) + + expect(paths).to contain_exactly( + 'templates/blank-yaml.yml', 'templates/dast/template.yml', 'templates/secret-detection.yml', + 'templates/template.yml' + ) + end + end + + describe '#extract_component_name' do + context 'with invalid component path' do + it 'raises an error' do + expect(components_project.extract_component_name('not-template/this-is-wrong.yml')).to be_nil + end + end + + context 'with valid component paths' do + where(:path, :name) do + 'templates/secret-detection.yml' | 'secret-detection' + 'templates/dast/template.yml' | 'dast' + 'templates/template.yml' | 'template' + 'templates/blank-yaml.yml' | 'blank-yaml' + end + + with_them do + it 'extracts the component name from the path' do + expect(components_project.extract_component_name(path)).to eq(name) + end + end + end + end + + describe '#extract_inputs' do + context 'with valid inputs' do + it 'extracts the inputs from a blob' do + blob = "spec:\n inputs:\n website:\n---\nimage: alpine_1" + + expect(components_project.extract_inputs(blob)).to eq({ website: nil }) + end + end + + context 'with invalid inputs' do + it 'raises InvalidFormatError' do + blob = "spec:\n inputs:\n website:\n---\nsome: invalid: string" + + expect do + components_project.extract_inputs(blob) + end.to raise_error(::Gitlab::Config::Loader::FormatError, + /mapping values are not allowed in this context/) + end + end + end + + describe '#fetch_component' do + where(:component_name, :content, :path) do + 'secret-detection' | "spec:\n inputs:\n website:\n---\nimage: alpine_1" | 'templates/secret-detection.yml' + 'dast' | 'image: alpine_2' | 'templates/dast/template.yml' + 'template' | 'image: alpine_3' | 'templates/template.yml' + 'blank-yaml' | '' | 'templates/blank-yaml.yml' + end + + with_them do + it 'fetches the content for a component' do + data = components_project.fetch_component(component_name) + + expect(data.path).to eq(path) + expect(data.content).to eq(content) + end + end + end +end diff --git a/spec/models/integrations/pushover_spec.rb b/spec/models/integrations/pushover_spec.rb index 8286fd20669..c576340a78a 100644 --- a/spec/models/integrations/pushover_spec.rb +++ b/spec/models/integrations/pushover_spec.rb @@ -62,4 +62,12 @@ RSpec.describe Integrations::Pushover do expect(WebMock).to have_requested(:post, 'https://8.8.8.8/1/messages.json').once end end + + describe '#avatar_url' do + it 'returns the avatar image path' do + expect(subject.avatar_url).to eq( + ActionController::Base.helpers.image_path('illustrations/third-party-logos/integrations-logos/pushover.svg') + ) + end + end end diff --git a/spec/models/ml/model_spec.rb b/spec/models/ml/model_spec.rb index 8cb4043df65..e22989f3ce2 100644 --- a/spec/models/ml/model_spec.rb +++ b/spec/models/ml/model_spec.rb @@ -119,6 +119,26 @@ RSpec.describe Ml::Model, feature_category: :mlops do end end + describe 'with_version_count' do + let(:model) { existing_model } + + subject { described_class.with_version_count.find_by(id: model.id).version_count } + + context 'when model has versions' do + before do + create(:ml_model_versions, model: model) + end + + it { is_expected.to eq(1) } + end + + context 'when model has no versions' do + let(:model) { another_existing_model } + + it { is_expected.to eq(0) } + end + end + describe '#by_project_and_id' do let(:id) { existing_model.id } let(:project_id) { existing_model.project.id } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 570a243154d..1a0fc34a1cd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6801,6 +6801,17 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr end end + describe '.with_package_registry_enabled' do + subject { described_class.with_package_registry_enabled } + + it 'returns projects with the package registry enabled' do + project_1 = create(:project) + create(:project, package_registry_access_level: ProjectFeature::DISABLED, packages_enabled: false) + + expect(subject).to contain_exactly(project_1) + end + end + describe '.deployments' do subject { project.deployments } diff --git a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb index d9ea7bc7f82..11f6d816fc1 100644 --- a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb @@ -72,6 +72,31 @@ RSpec.shared_context 'ProjectPolicyTable context' do :private | :disabled | :anonymous | nil | 0 end + # group_level, :membership, :admin_mode, :expected_count + # We need a new table because epics are at a group level only. + def permission_table_for_epics_access + :public | :admin | true | 1 + :public | :admin | false | 1 + :public | :reporter | nil | 1 + :public | :guest | nil | 1 + :public | :non_member | nil | 1 + :public | :anonymous | nil | 1 + + :internal | :admin | true | 1 + :internal | :admin | false | 0 + :internal | :reporter | nil | 0 + :internal | :guest | nil | 0 + :internal | :non_member | nil | 0 + :internal | :anonymous | nil | 0 + + :private | :admin | true | 1 + :private | :admin | false | 0 + :private | :reporter | nil | 0 + :private | :guest | nil | 0 + :private | :non_member | nil | 0 + :private | :anonymous | nil | 0 + end + # project_level, :feature_access_level, :membership, :admin_mode, :expected_count def permission_table_for_guest_feature_access :public | :enabled | :admin | true | 1 diff --git a/spec/workers/auto_devops/disable_worker_spec.rb b/spec/workers/auto_devops/disable_worker_spec.rb index 8f7f305b186..1d2b93b9287 100644 --- a/spec/workers/auto_devops/disable_worker_spec.rb +++ b/spec/workers/auto_devops/disable_worker_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'spec_helper' RSpec.describe AutoDevops::DisableWorker, '#perform', feature_category: :auto_devops do diff --git a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb index dc715c3026b..d11b044b093 100644 --- a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb +++ b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i let(:log_attributes) do { 'user_id' => user.id, - 'github_identifiers' => { 'id': gist_object.id }, + 'external_identifiers' => { 'id': gist_object.id }, 'class' => 'Gitlab::GithubGistsImport::ImportGistWorker', 'correlation_id' => 'new-correlation-id', 'jid' => nil, @@ -96,7 +96,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i it 'raises an error' do expect(Gitlab::GithubImport::Logger) .to receive(:error) - .with(log_attributes.merge('message' => 'importer failed', 'error.message' => '_some_error_')) + .with(log_attributes.merge('message' => 'importer failed', 'exception.message' => '_some_error_')) expect(Gitlab::ErrorTracking).to receive(:track_exception) expect { subject.perform(user.id, gist_hash, 'some_key') }.to raise_error(StandardError) @@ -113,7 +113,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i it 'tracks and logs error' do expect(Gitlab::GithubImport::Logger) .to receive(:error) - .with(log_attributes.merge('message' => 'importer failed', 'error.message' => 'error_message')) + .with(log_attributes.merge('message' => 'importer failed', 'exception.message' => 'error_message')) expect(Gitlab::JobWaiter) .to receive(:notify) .with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL) diff --git a/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb b/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb index 220f2bb0c75..0bd371b6c97 100644 --- a/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb +++ b/spec/workers/gitlab/github_gists_import/start_import_worker_spec.rb @@ -70,7 +70,7 @@ RSpec.describe Gitlab::GithubGistsImport::StartImportWorker, feature_category: : expect(Gitlab::GithubImport::Logger) .to receive(:error) - .with(log_attributes.merge('message' => 'import failed', 'error.message' => exception.message)) + .with(log_attributes.merge('message' => 'import failed', 'exception.message' => exception.message)) expect { worker.perform(user.id, token) }.to raise_error(StandardError) end diff --git a/spec/workers/integrations/execute_worker_spec.rb b/spec/workers/integrations/execute_worker_spec.rb index 369fc5fd091..10e290005cc 100644 --- a/spec/workers/integrations/execute_worker_spec.rb +++ b/spec/workers/integrations/execute_worker_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'spec_helper' RSpec.describe Integrations::ExecuteWorker, '#perform', feature_category: :integrations do diff --git a/spec/workers/partition_creation_worker_spec.rb b/spec/workers/partition_creation_worker_spec.rb index ab525fd5ce2..625e86ad852 100644 --- a/spec/workers/partition_creation_worker_spec.rb +++ b/spec/workers/partition_creation_worker_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -# + require 'spec_helper' RSpec.describe PartitionCreationWorker, feature_category: :database do diff --git a/spec/workers/projects/after_import_worker_spec.rb b/spec/workers/projects/after_import_worker_spec.rb index 5af4f49d6e0..18105488549 100644 --- a/spec/workers/projects/after_import_worker_spec.rb +++ b/spec/workers/projects/after_import_worker_spec.rb @@ -83,7 +83,7 @@ RSpec.describe Projects::AfterImportWorker, feature_category: :importers do message: 'Project housekeeping failed', project_full_path: project.full_path, project_id: project.id, - 'error.message' => exception.to_s + 'exception.message' => exception.to_s }).and_call_original subject diff --git a/spec/workers/projects/delete_branch_worker_spec.rb b/spec/workers/projects/delete_branch_worker_spec.rb index 771ab3def84..ddd65e51383 100644 --- a/spec/workers/projects/delete_branch_worker_spec.rb +++ b/spec/workers/projects/delete_branch_worker_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # rubocop: disable Gitlab/ServiceResponse require 'spec_helper' diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb index cd58dd93b80..1e82b0f2845 100644 --- a/spec/workers/web_hook_worker_spec.rb +++ b/spec/workers/web_hook_worker_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'spec_helper' RSpec.describe WebHookWorker, feature_category: :integrations do