diff --git a/.eslintrc.yml b/.eslintrc.yml index 72c33873ada..7f45fd912a9 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -99,7 +99,7 @@ rules: message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.' overrides: - files: - - '{,ee/,jh/}spec/frontend*/**/*' + - '{,ee/,jh/}spec/frontend*/**/*' rules: '@gitlab/require-i18n-strings': off '@gitlab/no-runtime-template-compiler': off @@ -139,11 +139,13 @@ overrides: parser: '@graphql-eslint/eslint-plugin' operations: - '{,ee/,jh/}app/**/*.graphql' - # You can run `bundle exec rake gitlab:graphql:schema:dump` and then uncomment this line - # schema: './tmp/tests/graphql/gitlab_schema.graphql' + # You can run `bundle exec rake gitlab:graphql:schema:dump` and then uncomment this line + # schema: './tmp/tests/graphql/gitlab_schema.graphql' rules: filenames/match-regex: off spaced-comment: off + # TODO: We need a way to include this rule + support ee_else_ce fragments + #'@graphql-eslint/unique-fragment-name': error # TODO: Uncomment these rules when then `schema` is available #'@graphql-eslint/fragments-on-composite-type': error #'@graphql-eslint/known-argument-names': error @@ -151,4 +153,3 @@ overrides: '@graphql-eslint/no-anonymous-operations': error '@graphql-eslint/unique-operation-name': error '@graphql-eslint/require-id-when-available': error - '@graphql-eslint/unique-fragment-name': error diff --git a/.gitlab/merge_request_templates/Removals.md b/.gitlab/merge_request_templates/Removals.md index 9d3738f63b5..159a07774ad 100644 --- a/.gitlab/merge_request_templates/Removals.md +++ b/.gitlab/merge_request_templates/Removals.md @@ -5,11 +5,11 @@ /milestone % /assign `@EM/PM` (choose the DRI; remove backticks here, and below) -**Be sure to link this MR to the relevant issue(s).** +**Be sure to link this MR to the relevant issues.** - Deprecation issue: - Removal issue: -- MR that removes the feature (optional): +- MR that removed (or _will_ remove) the feature: If there is no relevant deprecation issue, hit pause and: @@ -45,6 +45,7 @@ Please review the [guidelines for removals](https://about.gitlab.com/handbook/ma - [ ] Follow the process to [create a removal YAML file](https://about.gitlab.com/handbook/marketing/blog/release-posts/#creating-a-removal-entry). - [ ] Add reviewers by the 10th. - [ ] When ready to be merged and not later than the 15th, add the ~ready label and @ message the TW for final review and merge. + - Removal notices should not be merged before the code is removed from the product. Do not mark ~ready until the removal is complete, or you are certain it will be completed within the current milestone and released. If PMs are not sure, they should confirm with their Engineering Manager. ## Reviewers @@ -66,6 +67,8 @@ with the same process as regular docs MRs. Add suggestions as needed, @ message the PM to inform them the first review is complete, and remove yourself as a reviewer if it's not yet ready for merge. +**Removal notices should not be merged before the code is removed from the product.** +
Expand for Details diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 5cd7339fbfe..ac69a0f428c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -53768dae0bc6395d2b80438286723a912aa50921 +1b1b95408d11a2532db5a44ffefd5cbab6e0effd diff --git a/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql b/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql index 8c0f1762d91..df6ad0b712d 100644 --- a/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql +++ b/app/assets/javascripts/alerts_settings/graphql/fragments/http_integration_payload_data.fragment.graphql @@ -1,5 +1,3 @@ -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment HttpIntegrationPayloadData on AlertManagementHttpIntegration { id } diff --git a/app/assets/javascripts/boards/graphql/board.fragment.graphql b/app/assets/javascripts/boards/graphql/board.fragment.graphql index c4380a8744d..872a4c4afbc 100644 --- a/app/assets/javascripts/boards/graphql/board.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/board.fragment.graphql @@ -1,5 +1,3 @@ -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment BoardFragment on Board { id name diff --git a/app/assets/javascripts/boards/graphql/board_list.fragment.graphql b/app/assets/javascripts/boards/graphql/board_list.fragment.graphql index 5c7442dce68..bbf3314377e 100644 --- a/app/assets/javascripts/boards/graphql/board_list.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/board_list.fragment.graphql @@ -1,7 +1,5 @@ #import "./board_list_shared.fragment.graphql" -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment BoardListFragment on BoardList { ...BoardListShared } diff --git a/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql index 4e0a6246879..57f51822d91 100644 --- a/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/board_scope.fragment.graphql @@ -1,5 +1,3 @@ -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment BoardScopeFragment on Board { id name diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql index 8525b9065ca..53fe6fdc59e 100644 --- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql @@ -1,7 +1,5 @@ #import "~/graphql_shared/fragments/issue.fragment.graphql" -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment Issue on Issue { id ...IssueNode diff --git a/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql index 1fe85154d8d..78a368089a8 100644 --- a/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/iteration.fragment.graphql @@ -1,4 +1,4 @@ -fragment IterationShared on Iteration { +fragment Iteration on Iteration { id title } diff --git a/app/assets/javascripts/graphql_shared/fragments/page_info_cursors_only.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/page_info_cursors_only.fragment.graphql new file mode 100644 index 00000000000..22bcefbecd3 --- /dev/null +++ b/app/assets/javascripts/graphql_shared/fragments/page_info_cursors_only.fragment.graphql @@ -0,0 +1,4 @@ +fragment PageInfo on PageInfo { + startCursor + endCursor +} diff --git a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql index 5b2f0392232..b72941966c6 100644 --- a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql +++ b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql @@ -1,5 +1,4 @@ -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/require-id-when-available, @graphql-eslint/unique-fragment-name +# eslint-disable-next-line @graphql-eslint/require-id-when-available fragment IncidentFields on Issue { severity escalationStatus diff --git a/app/assets/javascripts/issues/list/queries/label.fragment.graphql b/app/assets/javascripts/issues/list/queries/label.fragment.graphql new file mode 100644 index 00000000000..bb1d8f1ac9b --- /dev/null +++ b/app/assets/javascripts/issues/list/queries/label.fragment.graphql @@ -0,0 +1,6 @@ +fragment Label on Label { + id + color + textColor + title +} diff --git a/app/assets/javascripts/issues/list/queries/search_labels.query.graphql b/app/assets/javascripts/issues/list/queries/search_labels.query.graphql index df563fe1925..44b57317161 100644 --- a/app/assets/javascripts/issues/list/queries/search_labels.query.graphql +++ b/app/assets/javascripts/issues/list/queries/search_labels.query.graphql @@ -1,4 +1,4 @@ -#import "~/graphql_shared/fragments/label.fragment.graphql" +#import "./label.fragment.graphql" query searchLabels($fullPath: ID!, $search: String, $isProject: Boolean = false) { group(fullPath: $fullPath) @skip(if: $isProject) { diff --git a/app/assets/javascripts/issues/list/queries/search_users.query.graphql b/app/assets/javascripts/issues/list/queries/search_users.query.graphql index 52c7963cb7c..46b48e4e41c 100644 --- a/app/assets/javascripts/issues/list/queries/search_users.query.graphql +++ b/app/assets/javascripts/issues/list/queries/search_users.query.graphql @@ -1,4 +1,4 @@ -#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "./user.fragment.graphql" query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false) { group(fullPath: $fullPath) @skip(if: $isProject) { diff --git a/app/assets/javascripts/issues/list/queries/user.fragment.graphql b/app/assets/javascripts/issues/list/queries/user.fragment.graphql new file mode 100644 index 00000000000..3e5bc0f7b93 --- /dev/null +++ b/app/assets/javascripts/issues/list/queries/user.fragment.graphql @@ -0,0 +1,6 @@ +fragment User on User { + id + avatarUrl + name + username +} diff --git a/app/assets/javascripts/repository/queries/commit.fragment.graphql b/app/assets/javascripts/repository/queries/commit.fragment.graphql index a6063c689dc..b046fc1f730 100644 --- a/app/assets/javascripts/repository/queries/commit.fragment.graphql +++ b/app/assets/javascripts/repository/queries/commit.fragment.graphql @@ -1,5 +1,3 @@ -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment TreeEntryCommit on LogTreeCommit { sha message diff --git a/app/assets/javascripts/repository/queries/path_locks.fragment.graphql b/app/assets/javascripts/repository/queries/path_locks.fragment.graphql index 96b6523361e..868a513362d 100644 --- a/app/assets/javascripts/repository/queries/path_locks.fragment.graphql +++ b/app/assets/javascripts/repository/queries/path_locks.fragment.graphql @@ -1,5 +1,3 @@ -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment ProjectPathLocksFragment on Project { id } diff --git a/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql index 4e30e30edc5..2449ee0fc0f 100644 --- a/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/details/runner_details.fragment.graphql @@ -1,7 +1,5 @@ #import "./runner_details_shared.fragment.graphql" -# This fragment is used as ee-else-ce -# eslint-disable-next-line @graphql-eslint/unique-fragment-name fragment RunnerDetails on CiRunner { ...RunnerDetailsShared } diff --git a/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql b/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql index b00ad3d483e..dceab61ed26 100644 --- a/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql +++ b/app/assets/javascripts/sidebar/queries/group_milestones.query.graphql @@ -12,7 +12,7 @@ query groupMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum includeAncestors: true ) { nodes { - ...SidebarMilestoneFragment + ...MilestoneFragment state } } diff --git a/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql index 9983c79d263..b0a16677cf2 100644 --- a/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql +++ b/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql @@ -8,7 +8,7 @@ query mergeRequestMilestone($fullPath: ID!, $iid: String!) { __typename id attribute: milestone { - ...SidebarMilestoneFragment + ...MilestoneFragment } } } diff --git a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql index 570e88966e5..d4f7e703692 100644 --- a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql +++ b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql @@ -1,4 +1,4 @@ -fragment SidebarMilestoneFragment on Milestone { +fragment MilestoneFragment on Milestone { id title webUrl: webPath diff --git a/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql b/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql index d481324e56a..c7f3adc9aca 100644 --- a/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql +++ b/app/assets/javascripts/sidebar/queries/project_issue_milestone.query.graphql @@ -8,7 +8,7 @@ query projectIssueMilestone($fullPath: ID!, $iid: String!) { __typename id attribute: milestone { - ...SidebarMilestoneFragment + ...MilestoneFragment } } } diff --git a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql index e431b331c5d..d9eab18628d 100644 --- a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql +++ b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql @@ -12,7 +12,7 @@ query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEn includeAncestors: true ) { nodes { - ...SidebarMilestoneFragment + ...MilestoneFragment state } } diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb index ccc38ba7cd5..908313bdb83 100644 --- a/app/controllers/admin/topics_controller.rb +++ b/app/controllers/admin/topics_controller.rb @@ -51,7 +51,8 @@ class Admin::TopicsController < Admin::ApplicationController [ :avatar, :description, - :name + :name, + :title ] end end diff --git a/app/graphql/queries/repository/files.query.graphql b/app/graphql/queries/repository/files.query.graphql index 5a352ee2a5a..a83880ce696 100644 --- a/app/graphql/queries/repository/files.query.graphql +++ b/app/graphql/queries/repository/files.query.graphql @@ -1,4 +1,4 @@ -fragment LocalPageInfo on PageInfo { +fragment PageInfo on PageInfo { __typename hasNextPage hasPreviousPage @@ -6,7 +6,7 @@ fragment LocalPageInfo on PageInfo { endCursor } -fragment LocalTreeEntry on Entry { +fragment TreeEntry on Entry { __typename id sha @@ -34,12 +34,12 @@ query getFiles( edges { __typename node { - ...LocalTreeEntry + ...TreeEntry webPath } } pageInfo { - ...LocalPageInfo + ...PageInfo } } submodules(first: $pageSize, after: $nextPageCursor) { @@ -47,13 +47,13 @@ query getFiles( edges { __typename node { - ...LocalTreeEntry + ...TreeEntry webUrl treeUrl } } pageInfo { - ...LocalPageInfo + ...PageInfo } } blobs(first: $pageSize, after: $nextPageCursor) { @@ -61,14 +61,14 @@ query getFiles( edges { __typename node { - ...LocalTreeEntry + ...TreeEntry mode webPath lfsOid } } pageInfo { - ...LocalPageInfo + ...PageInfo } } } diff --git a/app/models/project.rb b/app/models/project.rb index f7182d1645c..84db2b343e6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2903,7 +2903,7 @@ class Project < ApplicationRecord if @topic_list != self.topic_list self.topics.delete_all self.topics = @topic_list.map do |topic| - Projects::Topic.where('lower(name) = ?', topic.downcase).order(total_projects_count: :desc).first_or_create(name: topic) + Projects::Topic.where('lower(name) = ?', topic.downcase).order(total_projects_count: :desc).first_or_create(name: topic, title: topic) end end diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb index 9214a23e259..c21689a72d2 100644 --- a/app/models/projects/topic.rb +++ b/app/models/projects/topic.rb @@ -9,6 +9,7 @@ module Projects validates :name, presence: true, length: { maximum: 255 } validates :name, uniqueness: { case_sensitive: false }, if: :name_changed? + validates :title, presence: true, length: { maximum: 255 }, on: :create validates :description, length: { maximum: 1024 } has_many :project_topics, class_name: 'Projects::ProjectTopic' diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml index 50ef375dd35..9b9d97950cc 100644 --- a/app/views/admin/topics/_form.html.haml +++ b/app/views/admin/topics/_form.html.haml @@ -3,12 +3,19 @@ .form-group = f.label :name do - = _("Topic name") - = f.text_field :name, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' }, + = _("Topic slug (name)") + = f.text_field :name, placeholder: _('my-topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' }, required: true, title: _('Please fill in a name for your topic.'), autofocus: true + .form-group + = f.label :title do + = _("Topic title") + = f.text_field :title, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_title_field' }, + required: true, + title: _('Please fill in a title for your topic.') + .form-group = f.label :description, _("Description") = render layout: 'shared/md_preview', locals: { url: preview_markdown_admin_topics_path, referenced_users: false } do diff --git a/app/views/admin/topics/_topic.html.haml b/app/views/admin/topics/_topic.html.haml index 959e7ab31fc..462943263df 100644 --- a/app/views/admin/topics/_topic.html.haml +++ b/app/views/admin/topics/_topic.html.haml @@ -1,4 +1,5 @@ - topic = local_assigns.fetch(:topic) +- title = topic.title || topic.name %li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'topic_row_content' } } .avatar-container.rect-avatar.s40.gl-flex-shrink-0 @@ -6,7 +7,9 @@ .gl-min-w-0.gl-flex-grow-1 .title - = link_to topic.name, topic_explore_projects_path(topic_name: topic.name) + = link_to title, topic_explore_projects_path(topic_name: topic.name) + %div + = topic.name .stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex %span.gl-ml-5.has-tooltip{ title: n_('%d project', '%d projects', topic.total_projects_count) % topic.total_projects_count } diff --git a/data/deprecations/14-10-dependency-scanning-default-java-version.yml b/data/deprecations/14-10-dependency-scanning-default-java-version.yml index 4616592f00c..c589f2bb3ac 100644 --- a/data/deprecations/14-10-dependency-scanning-default-java-version.yml +++ b/data/deprecations/14-10-dependency-scanning-default-java-version.yml @@ -6,7 +6,7 @@ breaking_change: true reporter: NicoleSchwartz body: | - In GitLab 15.0, for Dependency Scanning, the default version of Java will be updated to 17. This is the same as [the most up-to-date Long Term Support (LTS) version](https://en.wikipedia.org/wiki/Java_version_history). GitLab still [supports the same versions as it does today (8, 11, 13, 14, 15, 16, 17)](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning). Only the enabled default is changing. To change the default, set the `DS_Java_Version` variable. + In GitLab 15.0, for Dependency Scanning, the default version of Java that the scanner expects will be updated from 11 to 17. Java 17 is [the most up-to-date Long Term Support (LTS) version](https://en.wikipedia.org/wiki/Java_version_history). Dependency scanning continues to support the same [range of versions (8, 11, 13, 14, 15, 16, 17)](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#supported-languages-and-package-managers), only the default version is changing. If your project uses the previous default of Java 11, be sure to [set the `DS_Java_Version` variable to match](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning). # The following items are not published on the docs page, but may be used in the future. stage: secure # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth tiers: ultimate # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] diff --git a/db/docs/group_features.yml b/db/docs/group_features.yml new file mode 100644 index 00000000000..a7b80f586ca --- /dev/null +++ b/db/docs/group_features.yml @@ -0,0 +1,9 @@ +--- +table_name: group_features +classes: +- Groups::FeatureSetting +feature_categories: +- subgroups +description: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82017 +milestone: '14.10' diff --git a/db/docs/project_build_artifacts_size_refreshes.yml b/db/docs/project_build_artifacts_size_refreshes.yml new file mode 100644 index 00000000000..8f07ab9b3e1 --- /dev/null +++ b/db/docs/project_build_artifacts_size_refreshes.yml @@ -0,0 +1,9 @@ +--- +table_name: project_build_artifacts_size_refreshes +classes: +- Projects::BuildArtifactsSizeRefresh +feature_categories: +- build_artifacts +description: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81306 +milestone: '14.9' diff --git a/db/docs/protected_environment_approval_rules.yml b/db/docs/protected_environment_approval_rules.yml new file mode 100644 index 00000000000..ea7f0e1d05d --- /dev/null +++ b/db/docs/protected_environment_approval_rules.yml @@ -0,0 +1,9 @@ +--- +table_name: protected_environment_approval_rules +classes: +- ProtectedEnvironments::ApprovalRule +feature_categories: +- continuous_delivery +description: TODO +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82800 +milestone: '14.10' diff --git a/db/migrate/20220331125725_add_title_to_topic.rb b/db/migrate/20220331125725_add_title_to_topic.rb new file mode 100644 index 00000000000..0c6ccb6beb9 --- /dev/null +++ b/db/migrate/20220331125725_add_title_to_topic.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddTitleToTopic < Gitlab::Database::Migration[1.0] + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20220331130726_add_text_limit_to_topics_title.rb + def change + add_column :topics, :title, :text + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20220331130726_add_text_limit_to_topics_title.rb b/db/migrate/20220331130726_add_text_limit_to_topics_title.rb new file mode 100644 index 00000000000..4a4b30edc0d --- /dev/null +++ b/db/migrate/20220331130726_add_text_limit_to_topics_title.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitToTopicsTitle < Gitlab::Database::Migration[1.0] + disable_ddl_transaction! + + def up + add_text_limit :topics, :title, 255 + end + + def down + remove_text_limit :topics, :title + end +end diff --git a/db/post_migrate/20220331133802_schedule_backfill_topics_title.rb b/db/post_migrate/20220331133802_schedule_backfill_topics_title.rb new file mode 100644 index 00000000000..8e594a9df52 --- /dev/null +++ b/db/post_migrate/20220331133802_schedule_backfill_topics_title.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ScheduleBackfillTopicsTitle < Gitlab::Database::Migration[1.0] + MIGRATION = 'BackfillTopicsTitle' + DELAY_INTERVAL = 2.minutes + + disable_ddl_transaction! + + def up + queue_background_migration_jobs_by_range_at_intervals( + define_batchable_model('topics'), + MIGRATION, + DELAY_INTERVAL, + track_jobs: true + ) + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20220331125725 b/db/schema_migrations/20220331125725 new file mode 100644 index 00000000000..6f10644b517 --- /dev/null +++ b/db/schema_migrations/20220331125725 @@ -0,0 +1 @@ +a7e5490e9b53cbbed01c03690cbe0bb4668adb17ec4fe14ca96e021f2e313b38 \ No newline at end of file diff --git a/db/schema_migrations/20220331130726 b/db/schema_migrations/20220331130726 new file mode 100644 index 00000000000..d88e56d14c9 --- /dev/null +++ b/db/schema_migrations/20220331130726 @@ -0,0 +1 @@ +bcaf6139100dc5658d33292e8e5484d1d6278f022eeb6e3bcd519efdccdf4470 \ No newline at end of file diff --git a/db/schema_migrations/20220331133802 b/db/schema_migrations/20220331133802 new file mode 100644 index 00000000000..780876ac02e --- /dev/null +++ b/db/schema_migrations/20220331133802 @@ -0,0 +1 @@ +0e96430b245f6f04447ee50b6e0c0b9d7828cfeaf1f08e303aa04bb40a117a7f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e096090e75d..7259528a335 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -21050,6 +21050,8 @@ CREATE TABLE topics ( description text, total_projects_count bigint DEFAULT 0 NOT NULL, non_private_projects_count bigint DEFAULT 0 NOT NULL, + title text, + CONSTRAINT check_223b50f9be CHECK ((char_length(title) <= 255)), CONSTRAINT check_26753fb43a CHECK ((char_length(avatar) <= 255)), CONSTRAINT check_5d1a07c8c8 CHECK ((char_length(description) <= 1024)), CONSTRAINT check_7a90d4c757 CHECK ((char_length(name) <= 255)) diff --git a/doc/api/projects.md b/doc/api/projects.md index 7707d1f990d..0d39fdd3aa7 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1422,7 +1422,7 @@ Supported attributes: | `emails_disabled` | boolean | **{dotted-circle}** No | Disable email notifications. | | `external_authorization_classification_label` **(PREMIUM)** | string | **{dotted-circle}** No | The classification label for the project. | | `forking_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. | -| `import_url` | string | **{dotted-circle}** No | URL to import repository from. | +| `import_url` | string | **{dotted-circle}** No | URL the repository was imported from. | | `issues_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. | | `issues_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable issues for this project. Use `issues_access_level` instead. | | `issues_template` **(PREMIUM)** | string | **{dotted-circle}** No | Default description for Issues. Description is parsed with GitLab Flavored Markdown. See [Templates for issues and merge requests](#templates-for-issues-and-merge-requests). | diff --git a/doc/api/topics.md b/doc/api/topics.md index 1246e36f2cb..78f32fb8fa0 100644 --- a/doc/api/topics.md +++ b/doc/api/topics.md @@ -38,21 +38,24 @@ Example response: [ { "id": 1, - "name": "GitLab", + "name": "gitlab", + "title": "GitLab", "description": "GitLab is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more.", "total_projects_count": 1000, "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon" }, { "id": 3, - "name": "Git", + "name": "git", + "title": "Git", "description": "Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.", "total_projects_count": 900, "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon" }, { "id": 2, - "name": "Git LFS", + "name": "git-lfs", + "title": "Git LFS", "description": null, "total_projects_count": 300, "avatar_url": null @@ -85,7 +88,8 @@ Example response: ```json { "id": 1, - "name": "GitLab", + "name": "gitlab", + "title": "GitLab", "description": "GitLab is an open source end-to-end software development platform with built-in version control, issue tracking, code review, CI/CD, and more.", "total_projects_count": 1000, "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon" @@ -112,7 +116,8 @@ Supported attributes: | Attribute | Type | Required | Description | | ------------- | ------- | ---------------------- | ----------- | -| `name` | string | **{check-circle}** Yes | Name | +| `name` | string | **{check-circle}** Yes | Slug (name) | +| `title` | string | **{check-circle}** Yes | Title | | `avatar` | file | **{dotted-circle}** No | Avatar | | `description` | string | **{dotted-circle}** No | Description | @@ -120,7 +125,7 @@ Example request: ```shell curl --request POST \ - --data "name=topic1" \ + --data "name=topic1&title=Topic 1" \ --header "PRIVATE-TOKEN: " \ "https://gitlab.example.com/api/v4/topics" ``` @@ -131,6 +136,7 @@ Example response: { "id": 1, "name": "topic1", + "title": "Topic 1", "description": null, "total_projects_count": 0, "avatar_url": null @@ -152,7 +158,8 @@ Supported attributes: | `id` | integer | **{check-circle}** Yes | ID of project topic | | `avatar` | file | **{dotted-circle}** No | Avatar | | `description` | string | **{dotted-circle}** No | Description | -| `name` | string | **{dotted-circle}** No | Name | +| `name` | string | **{dotted-circle}** No | Slug (name) | +| `title` | string | **{dotted-circle}** No | Title | Example request: @@ -169,6 +176,7 @@ Example response: { "id": 1, "name": "topic1", + "title": "Topic 1", "description": null, "total_projects_count": 0, "avatar_url": null diff --git a/doc/development/documentation/feature_flags.md b/doc/development/documentation/feature_flags.md index fb58851e93f..dd1cd61ddb1 100644 --- a/doc/development/documentation/feature_flags.md +++ b/doc/development/documentation/feature_flags.md @@ -45,7 +45,7 @@ You can combine entries if they happened in the same release: ## Use a note to describe the state of the feature flag -Information about feature flags should be in a **Note** at the start of the topic (just below the version history). +Information about feature flags should be in a `FLAG` note at the start of the topic (just below the version history). The note has three parts, and follows this structure: @@ -62,6 +62,7 @@ FLAG: |--------------------------|---------------| | Available | `On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](/administration/feature_flags.md) named .` | | Unavailable | `On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](/administration/feature_flags.md) named .` | +| Available to some users | `On self-managed GitLab, by default this feature is available to a subset of users. To show or hide the feature for all, ask an administrator to [change the status of the feature flag](/administration/feature_flags.md) named .` | | Available, per-group | `On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the feature flag](/administration/feature_flags.md) named .` | | Unavailable, per-group | `On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](/administration/feature_flags.md) named .` | | Available, per-project | `On self-managed GitLab, by default this feature is available. To hide the feature per project or for your entire instance, ask an administrator to [disable the feature flag](/administration/feature_flags.md) named .` | diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index f77a6c8eec2..7373ad6030c 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -52,7 +52,7 @@ as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#brea Before updating GitLab, review the details carefully to determine if you need to make any changes to your code, settings, or workflow. -In GitLab 15.0, for Dependency Scanning, the default version of Java will be updated to 17. This is the same as [the most up-to-date Long Term Support (LTS) version](https://en.wikipedia.org/wiki/Java_version_history). GitLab still [supports the same versions as it does today (8, 11, 13, 14, 15, 16, 17)](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning). Only the enabled default is changing. To change the default, set the `DS_Java_Version` variable. +In GitLab 15.0, for Dependency Scanning, the default version of Java that the scanner expects will be updated from 11 to 17. Java 17 is [the most up-to-date Long Term Support (LTS) version](https://en.wikipedia.org/wiki/Java_version_history). Dependency scanning continues to support the same [range of versions (8, 11, 13, 14, 15, 16, 17)](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#supported-languages-and-package-managers), only the default version is changing. If your project uses the previous default of Java 11, be sure to [set the `DS_Java_Version` variable to match](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning). **Planned removal milestone: 15.0 (2021-05-22)** diff --git a/doc/user/infrastructure/iac/index.md b/doc/user/infrastructure/iac/index.md index bc7a3c0d069..37659c88827 100644 --- a/doc/user/infrastructure/iac/index.md +++ b/doc/user/infrastructure/iac/index.md @@ -107,13 +107,9 @@ workflows. ## The GitLab Terraform provider -NOTE: -The GitLab Terraform provider is released separately from GitLab. -We are working on migrating the GitLab Terraform provider to GitLab.com. - -The [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) is a plugin for Terraform to facilitate -managing of GitLab resources such as users, groups, and projects. -Its documentation is available on [Terraform](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs). +The [GitLab Terraform provider](https://github.com/gitlabhq/terraform-provider-gitlab) is a Terraform plugin to facilitate +managing of GitLab resources such as users, groups, and projects. It is released separately from GitLab +and its documentation is available on [Terraform](https://registry.terraform.io/providers/gitlabhq/gitlab/latest/docs). ## Create a new cluster through IaC diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 30261ed5082..589cdd32bef 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -41,7 +41,7 @@ Prerequisites: To export a project and its data, follow these steps: 1. On the top bar, select **Menu > Projects** and find your project. -1. On the left sidebar, select **Settings**. +1. On the left sidebar, select **Settings > General**. 1. Expand **Advanced**. 1. Select **Export project**. 1. After the export is generated, you should receive an email with a link to download the file. diff --git a/lib/api/entities/projects/topic.rb b/lib/api/entities/projects/topic.rb index d3d1cbec81c..976c307382a 100644 --- a/lib/api/entities/projects/topic.rb +++ b/lib/api/entities/projects/topic.rb @@ -6,6 +6,7 @@ module API class Topic < Grape::Entity expose :id expose :name + expose :title expose :description expose :total_projects_count expose :avatar_url do |topic, options| diff --git a/lib/api/topics.rb b/lib/api/topics.rb index e4a1fa2367e..15f79e75be3 100644 --- a/lib/api/topics.rb +++ b/lib/api/topics.rb @@ -38,7 +38,8 @@ module API success Entities::Projects::Topic end params do - requires :name, type: String, desc: 'Name' + requires :name, type: String, desc: 'Slug (name)' + requires :title, type: String, desc: 'Title' optional :description, type: String, desc: 'Description' optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for topic' end @@ -60,7 +61,8 @@ module API end params do requires :id, type: Integer, desc: 'ID of project topic' - optional :name, type: String, desc: 'Name' + optional :name, type: String, desc: 'Slug (name)' + optional :title, type: String, desc: 'Title' optional :description, type: String, desc: 'Description' optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for topic' end diff --git a/lib/gitlab/background_migration/backfill_topics_title.rb b/lib/gitlab/background_migration/backfill_topics_title.rb new file mode 100644 index 00000000000..19a1eff5b58 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_topics_title.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # The class to backfill the topic title + class BackfillTopicsTitle + # Temporary AR model for topics + class Topic < ActiveRecord::Base + self.table_name = 'topics' + end + + def perform(start_id, end_id) + Topic.where(id: start_id..end_id).where(title: nil).update_all('title = name') + + mark_job_as_succeeded(start_id, end_id) + end + + private + + def mark_job_as_succeeded(*arguments) + ::Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded( + self.class.name.demodulize, + arguments + ) + end + end + end +end diff --git a/lib/support/systemd/gitlab-sidekiq.service b/lib/support/systemd/gitlab-sidekiq.service index 81046f5348a..7d09944c862 100644 --- a/lib/support/systemd/gitlab-sidekiq.service +++ b/lib/support/systemd/gitlab-sidekiq.service @@ -6,7 +6,7 @@ After=network.target JoinsNamespaceOf=gitlab-puma.service [Service] -Type=simple +Type=notify User=git WorkingDirectory=/home/git/gitlab Environment=RAILS_ENV=production @@ -17,6 +17,7 @@ Restart=on-failure RestartSec=1 SyslogIdentifier=gitlab-sidekiq Slice=gitlab.slice +WatchdogSec=10 [Install] WantedBy=gitlab.target diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e08c22b8b3b..b906fdaadaf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -28294,6 +28294,9 @@ msgstr "" msgid "Please fill in a name for your topic." msgstr "" +msgid "Please fill in a title for your topic." +msgstr "" + msgid "Please fill out this field." msgstr "" @@ -39730,7 +39733,10 @@ msgstr "" msgid "Topic avatar for %{name} will be removed. This cannot be undone." msgstr "" -msgid "Topic name" +msgid "Topic slug (name)" +msgstr "" + +msgid "Topic title" msgstr "" msgid "Topic was successfully updated." @@ -45344,6 +45350,9 @@ msgstr "" msgid "my-channel" msgstr "" +msgid "my-topic" +msgstr "" + msgid "need attention" msgstr "" diff --git a/scripts/trigger-build.rb b/scripts/trigger-build.rb index a3356c664d1..c86279ce952 100755 --- a/scripts/trigger-build.rb +++ b/scripts/trigger-build.rb @@ -83,7 +83,7 @@ module Trigger # Override to trigger and work with pipeline on different GitLab instance # type: :downstream -> downstream build and pipeline status # type: :upstream -> this project, e.g. for posting comments - def gitlab_client(type) + def gitlab_client(_type) # By default, always use the same client @gitlab_client ||= Gitlab.client( endpoint: 'https://gitlab.com/api/v4', @@ -97,10 +97,15 @@ module Trigger end # Must be overridden - def ref + def ref_param_name raise NotImplementedError end + # Can be overridden + def primary_ref + 'main' + end + # Can be overridden def trigger_token ENV['CI_JOB_TOKEN'] @@ -116,6 +121,27 @@ module Trigger ENV[version_file]&.strip || File.read(version_file).strip end + # Can be overridden + def trigger_stable_branch_if_detected? + false + end + + def stable_branch? + ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/ + end + + def fallback_ref + if trigger_stable_branch_if_detected? && stable_branch? + ENV['CI_COMMIT_REF_NAME'].delete_suffix('-ee') + else + primary_ref + end + end + + def ref + ENV.fetch(ref_param_name, fallback_ref) + end + def base_variables # Use CI_MERGE_REQUEST_SOURCE_BRANCH_SHA for omnibus checkouts due to pipeline for merged results, # and fallback to CI_COMMIT_SHA for the `detached` pipelines. @@ -146,8 +172,16 @@ module Trigger ENV.fetch('OMNIBUS_PROJECT_PATH', 'gitlab-org/build/omnibus-gitlab-mirror') end - def ref - ENV.fetch('OMNIBUS_BRANCH', 'master') + def ref_param_name + 'OMNIBUS_BRANCH' + end + + def primary_ref + 'master' + end + + def trigger_stable_branch_if_detected? + true end def extra_variables @@ -184,10 +218,16 @@ module Trigger private - def ref - return ENV['CI_COMMIT_REF_NAME'] if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/ + def ref_param_name + 'CNG_BRANCH' + end - ENV.fetch('CNG_BRANCH', 'master') + def primary_ref + 'master' + end + + def trigger_stable_branch_if_detected? + true end def extra_variables @@ -272,8 +312,8 @@ module Trigger ENV.fetch('DOCS_PROJECT_PATH', 'gitlab-org/gitlab-docs') end - def ref - ENV.fetch('DOCS_BRANCH', 'main') + def ref_param_name + 'DOCS_BRANCH' end # `gitlab-org/gitlab-docs` pipeline trigger "Triggered from gitlab-org/gitlab 'review-docs-deploy' job" @@ -377,8 +417,12 @@ module Trigger } end - def ref - ENV['GITLABCOM_DATABASE_TESTING_TRIGGER_REF'] || 'master' + def ref_param_name + 'GITLABCOM_DATABASE_TESTING_TRIGGER_REF' + end + + def primary_ref + 'master' end end diff --git a/spec/controllers/admin/topics_controller_spec.rb b/spec/controllers/admin/topics_controller_spec.rb index ea510f916da..67943525687 100644 --- a/spec/controllers/admin/topics_controller_spec.rb +++ b/spec/controllers/admin/topics_controller_spec.rb @@ -77,24 +77,31 @@ RSpec.describe Admin::TopicsController do describe 'POST #create' do it 'creates topic' do expect do - post :create, params: { projects_topic: { name: 'test' } } + post :create, params: { projects_topic: { name: 'test', title: 'Test' } } end.to change { Projects::Topic.count }.by(1) end - it 'shows error message for invalid topic' do - post :create, params: { projects_topic: { name: nil } } + it 'shows error message for invalid topic name' do + post :create, params: { projects_topic: { name: nil, title: 'Test' } } errors = assigns[:topic].errors expect(errors).to contain_exactly(errors.full_message(:name, I18n.t('errors.messages.blank'))) end - it 'shows error message if topic not unique (case insensitive)' do - post :create, params: { projects_topic: { name: topic.name.upcase } } + it 'shows error message if topic name not unique (case insensitive)' do + post :create, params: { projects_topic: { name: topic.name.upcase, title: topic.title } } errors = assigns[:topic].errors expect(errors).to contain_exactly(errors.full_message(:name, I18n.t('errors.messages.taken'))) end + it 'shows error message for invalid topic title' do + post :create, params: { projects_topic: { name: 'test', title: nil } } + + errors = assigns[:topic].errors + expect(errors).to contain_exactly(errors.full_message(:title, I18n.t('errors.messages.blank'))) + end + context 'as a normal user' do before do sign_in(user) diff --git a/spec/db/docs_spec.rb b/spec/db/docs_spec.rb new file mode 100644 index 00000000000..20746e107fb --- /dev/null +++ b/spec/db/docs_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Database Documentation' do + context 'for each table' do + let(:all_tables) do + Gitlab::Database.database_base_models.flat_map { |_, m| m.connection.tables }.sort.uniq + end + + let(:metadata_required_fields) do + %i( + feature_categories + table_name + ) + end + + let(:metadata_allowed_fields) do + metadata_required_fields + %i( + classes + description + introduced_by_url + milestone + ) + end + + let(:metadata) do + all_tables.each_with_object({}) do |table_name, hash| + next unless File.exist?(table_metadata_file_path(table_name)) + + hash[table_name] ||= load_table_metadata(table_name) + end + end + + let(:tables_without_metadata) do + all_tables.reject { |t| metadata.has_key?(t) } + end + + let(:tables_without_valid_metadata) do + metadata.select { |_, t| t.has_key?(:error) }.keys + end + + let(:tables_with_disallowed_fields) do + metadata.select { |_, t| t.has_key?(:disallowed_fields) }.keys + end + + let(:tables_with_missing_required_fields) do + metadata.select { |_, t| t.has_key?(:missing_required_fields) }.keys + end + + it 'has a metadata file' do + expect(tables_without_metadata).to be_empty, multiline_error( + 'Missing metadata files', + tables_without_metadata.map { |t| " #{table_metadata_file(t)}" } + ) + end + + it 'has a valid metadata file' do + expect(tables_without_valid_metadata).to be_empty, table_metadata_errors( + 'Table metadata files with errors', + :error, + tables_without_valid_metadata + ) + end + + it 'has a valid metadata file with allowed fields' do + expect(tables_with_disallowed_fields).to be_empty, table_metadata_errors( + 'Table metadata files with disallowed fields', + :disallowed_fields, + tables_with_disallowed_fields + ) + end + + it 'has a valid metadata file without missing fields' do + expect(tables_with_missing_required_fields).to be_empty, table_metadata_errors( + 'Table metadata files with missing fields', + :missing_required_fields, + tables_with_missing_required_fields + ) + end + end + + private + + def table_metadata_file(table_name) + File.join('db', 'docs', "#{table_name}.yml") + end + + def table_metadata_file_path(table_name) + Rails.root.join(table_metadata_file(table_name)) + end + + def load_table_metadata(table_name) + result = {} + begin + result[:metadata] = YAML.safe_load(File.read(table_metadata_file_path(table_name))).deep_symbolize_keys + + disallowed_fields = (result[:metadata].keys - metadata_allowed_fields) + unless disallowed_fields.empty? + result[:disallowed_fields] = "fields not allowed: #{disallowed_fields.join(', ')}" + end + + missing_required_fields = (metadata_required_fields - result[:metadata].reject { |_, v| v.blank? }.keys) + unless missing_required_fields.empty? + result[:missing_required_fields] = "missing required fields: #{missing_required_fields.join(', ')}" + end + rescue Psych::SyntaxError => ex + result[:error] = ex.message + end + result + end + + def table_metadata_errors(title, field, tables) + lines = tables.map do |table_name| + <<~EOM + #{table_metadata_file(table_name)} + #{metadata[table_name][field]} + EOM + end + + multiline_error(title, lines) + end + + def multiline_error(title, lines) + <<~EOM + #{title}: + + #{lines.join("\n")} + EOM + end +end diff --git a/spec/factories/topics.rb b/spec/factories/topics.rb index e77441d9eae..a6e614e0c66 100644 --- a/spec/factories/topics.rb +++ b/spec/factories/topics.rb @@ -3,5 +3,6 @@ FactoryBot.define do factory :topic, class: 'Projects::Topic' do name { generate(:name) } + title { generate(:title) } end end diff --git a/spec/lib/api/entities/projects/topic_spec.rb b/spec/lib/api/entities/projects/topic_spec.rb index cdf142dbb7d..1ea0e724fed 100644 --- a/spec/lib/api/entities/projects/topic_spec.rb +++ b/spec/lib/api/entities/projects/topic_spec.rb @@ -11,6 +11,7 @@ RSpec.describe API::Entities::Projects::Topic do expect(subject).to include( :id, :name, + :title, :description, :total_projects_count, :avatar_url diff --git a/spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb b/spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb new file mode 100644 index 00000000000..3c46456eed0 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillTopicsTitle, schema: 20220331133802 do + it 'correctly backfills the title of the topics' do + topics = table(:topics) + + topic_1 = topics.create!(name: 'topic1') + topic_2 = topics.create!(name: 'topic2', title: 'Topic 2') + topic_3 = topics.create!(name: 'topic3') + topic_4 = topics.create!(name: 'topic4') + + subject.perform(topic_1.id, topic_3.id) + + expect(topic_1.reload.title).to eq('topic1') + expect(topic_2.reload.title).to eq('Topic 2') + expect(topic_3.reload.title).to eq('topic3') + expect(topic_4.reload.title).to be_nil + end +end diff --git a/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb index 254b4fea698..2c2c048992f 100644 --- a/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb +++ b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 20220223124428 do +RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 20220331133802 do def set_avatar(topic_id, avatar) topic = ::Projects::Topic.find(topic_id) topic.avatar = avatar @@ -16,49 +16,62 @@ RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 202 topics = table(:topics) project_topics = table(:project_topics) - group = namespaces.create!(name: 'group', path: 'group') - project_1 = projects.create!(namespace_id: group.id, visibility_level: 20) - project_2 = projects.create!(namespace_id: group.id, visibility_level: 10) - project_3 = projects.create!(namespace_id: group.id, visibility_level: 0) + group_1 = namespaces.create!(name: 'space1', type: 'Group', path: 'space1') + group_2 = namespaces.create!(name: 'space2', type: 'Group', path: 'space2') + group_3 = namespaces.create!(name: 'space3', type: 'Group', path: 'space3') + proj_space_1 = namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: group_1.id) + proj_space_2 = namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: group_2.id) + proj_space_3 = namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: group_3.id) + project_1 = projects.create!(namespace_id: group_1.id, project_namespace_id: proj_space_1.id, visibility_level: 20) + project_2 = projects.create!(namespace_id: group_2.id, project_namespace_id: proj_space_2.id, visibility_level: 10) + project_3 = projects.create!(namespace_id: group_3.id, project_namespace_id: proj_space_3.id, visibility_level: 0) topic_1_keep = topics.create!( name: 'topic1', + title: 'Topic 1', description: 'description 1 to keep', total_projects_count: 2, non_private_projects_count: 2 ) topic_1_remove = topics.create!( name: 'TOPIC1', + title: 'Topic 1', description: 'description 1 to remove', total_projects_count: 2, non_private_projects_count: 1 ) topic_2_remove = topics.create!( name: 'topic2', + title: 'Topic 2', total_projects_count: 0 ) topic_2_keep = topics.create!( name: 'TOPIC2', + title: 'Topic 2', description: 'description 2 to keep', total_projects_count: 1 ) topic_3_remove_1 = topics.create!( name: 'topic3', + title: 'Topic 3', total_projects_count: 2, non_private_projects_count: 1 ) topic_3_keep = topics.create!( name: 'Topic3', + title: 'Topic 3', total_projects_count: 2, non_private_projects_count: 2 ) topic_3_remove_2 = topics.create!( name: 'TOPIC3', + title: 'Topic 3', description: 'description 3 to keep', total_projects_count: 2, non_private_projects_count: 1 ) topic_4_keep = topics.create!( - name: 'topic4' + name: 'topic4', + title: 'Topic 4' ) project_topics_1 = [] diff --git a/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb b/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb new file mode 100644 index 00000000000..13e8c42269b --- /dev/null +++ b/spec/migrations/20220331133802_schedule_backfill_topics_title_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleBackfillTopicsTitle do + let(:topics) { table(:topics) } + + let!(:topic1) { topics.create!(name: 'topic1') } + let!(:topic2) { topics.create!(name: 'topic2') } + let!(:topic3) { topics.create!(name: 'topic3') } + + it 'correctly schedules background migrations', :aggregate_failures do + stub_const("#{Gitlab::Database::Migrations::BackgroundMigrationHelpers}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, topic1.id, topic2.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, topic3.id, topic3.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/models/projects/topic_spec.rb b/spec/models/projects/topic_spec.rb index 8fc4d11f0d9..e8249bc55e5 100644 --- a/spec/models/projects/topic_spec.rb +++ b/spec/models/projects/topic_spec.rb @@ -25,6 +25,8 @@ RSpec.describe Projects::Topic do it { is_expected.to validate_uniqueness_of(:name).case_insensitive } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(1024) } + it { expect(Projects::Topic.new).to validate_presence_of(:title) } + it { expect(Projects::Topic.new).to validate_length_of(:title).is_at_most(255) } end describe 'scopes' do diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index 5c17ca9581e..e711414a895 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -117,7 +117,7 @@ RSpec.describe API::Topics do describe 'POST /topics', :aggregate_failures do context 'as administrator' do it 'creates a topic' do - post api('/topics/', admin), params: { name: 'my-topic' } + post api('/topics/', admin), params: { name: 'my-topic', title: 'My Topic' } expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq('my-topic') @@ -128,7 +128,7 @@ RSpec.describe API::Topics do workhorse_form_with_file( api('/topics/', admin), file_key: :avatar, - params: { name: 'my-topic', description: 'my description...', avatar: file } + params: { name: 'my-topic', title: 'My Topic', description: 'my description...', avatar: file } ) expect(response).to have_gitlab_http_status(:created) @@ -137,23 +137,30 @@ RSpec.describe API::Topics do end it 'returns 400 if name is missing' do - post api('/topics/', admin) + post api('/topics/', admin), params: { title: 'My Topic' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('name is missing') end it 'returns 400 if name is not unique (case insensitive)' do - post api('/topics/', admin), params: { name: topic_1.name.downcase } + post api('/topics/', admin), params: { name: topic_1.name.downcase, title: 'My Topic' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['name']).to eq(['has already been taken']) end + + it 'returns 400 if title is missing' do + post api('/topics/', admin), params: { name: 'my-topic' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eql('title is missing') + end end context 'as normal user' do it 'returns 403 Forbidden' do - post api('/topics/', user), params: { name: 'my-topic' } + post api('/topics/', user), params: { name: 'my-topic', title: 'My Topic' } expect(response).to have_gitlab_http_status(:forbidden) end @@ -161,7 +168,7 @@ RSpec.describe API::Topics do context 'as anonymous' do it 'returns 401 Unauthorized' do - post api('/topics/'), params: { name: 'my-topic' } + post api('/topics/'), params: { name: 'my-topic', title: 'My Topic' } expect(response).to have_gitlab_http_status(:unauthorized) end diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb new file mode 100644 index 00000000000..18e57966bc2 --- /dev/null +++ b/spec/scripts/trigger-build_spec.rb @@ -0,0 +1,936 @@ +# frozen_string_literal: true +# rubocop:disable RSpec/VerifiedDoubles + +require 'fast_spec_helper' +require 'rspec-parameterized' + +require_relative '../../scripts/trigger-build' + +RSpec.describe Trigger do + let(:env) do + { + 'CI_JOB_URL' => 'ci_job_url', + 'CI_PROJECT_PATH' => 'ci_project_path', + 'CI_COMMIT_REF_NAME' => 'ci_commit_ref_name', + 'CI_COMMIT_REF_SLUG' => 'ci_commit_ref_slug', + 'CI_COMMIT_SHA' => 'ci_commit_sha', + 'CI_MERGE_REQUEST_PROJECT_ID' => 'ci_merge_request_project_id', + 'CI_MERGE_REQUEST_IID' => 'ci_merge_request_iid', + 'GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN' => 'bot-token', + 'CI_JOB_TOKEN' => 'job-token', + 'GITLAB_USER_NAME' => 'gitlab_user_name', + 'GITLAB_USER_LOGIN' => 'gitlab_user_login', + 'QA_IMAGE' => 'qa_image' + } + end + + let(:stubbed_downstream_gitlab_client) { double('downstream_gitlab_api_client') } + let(:gitlab_client_private_token) { env['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] } + let(:stubbed_pipeline) { Struct.new(:id, :web_url).new(42, 'pipeline_url') } + let(:trigger_token) { env['CI_JOB_TOKEN'] } + let(:api_endpoint) { 'https://gitlab.com/api/v4' } + + before do + stub_env(env) + allow(subject).to receive(:puts) + allow(Gitlab).to receive(:client) + .with( + endpoint: api_endpoint, + private_token: gitlab_client_private_token + ) + .and_return(stubbed_downstream_gitlab_client) + end + + def expect_run_trigger_with_params(variables = {}) + expect(stubbed_downstream_gitlab_client).to receive(:run_trigger) + .with( + downstream_project_path, + trigger_token, + ref, + hash_including(variables) + ) + .and_return(stubbed_pipeline) + end + + describe Trigger::Base do + let(:ref) { 'main' } + + describe '#invoke!' do + context "when required methods aren't defined" do + it 'raises a NotImplementedError' do + expect { described_class.new.invoke! }.to raise_error(NotImplementedError) + end + end + + context "when required methods are defined" do + let(:downstream_project_path) { 'foo/bar' } + let(:subclass) do + Class.new(Trigger::Base) do + def downstream_project_path + 'foo/bar' + end + + # Must be overridden + def ref_param_name + 'FOO_BAR_BRANCH' + end + end + end + + subject { subclass.new } + + context 'when env variable `FOO_BAR_BRANCH` does not exist' do + it 'triggers the pipeline on the correct project and branch' do + expect_run_trigger_with_params + + subject.invoke! + end + end + + context 'when env variable `FOO_BAR_BRANCH` exists' do + let(:ref) { 'foo_bar_branch' } + + before do + stub_env('FOO_BAR_BRANCH', ref) + end + + it 'triggers the pipeline on the correct project and branch' do + expect_run_trigger_with_params + + subject.invoke! + end + end + + it 'waits for downstream pipeline' do + expect_run_trigger_with_params + expect(Trigger::Pipeline).to receive(:new) + .with(downstream_project_path, stubbed_pipeline.id, stubbed_downstream_gitlab_client) + + subject.invoke! + end + + context 'with post_comment: true' do + before do + stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee") + end + + it 'posts a comment' do + expect_run_trigger_with_params + expect(Trigger::CommitComment).to receive(:post!).with(stubbed_pipeline, stubbed_downstream_gitlab_client) + + subject.invoke!(post_comment: true) + end + end + + context 'with downstream_job_name: "foo"' do + let(:downstream_job) { Struct.new(:id, :name).new(42, 'foo') } + let(:paginated_resources) { Struct.new(:auto_paginate).new([downstream_job]) } + + before do + stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee") + end + + it 'fetches the downstream job' do + expect_run_trigger_with_params + expect(stubbed_downstream_gitlab_client).to receive(:pipeline_jobs) + .with(downstream_project_path, stubbed_pipeline.id).and_return(paginated_resources) + expect(Trigger::Job).to receive(:new) + .with(downstream_project_path, downstream_job.id, stubbed_downstream_gitlab_client) + + subject.invoke!(downstream_job_name: 'foo') + end + end + end + end + + describe '#variables' do + let(:simple_forwarded_variables) do + { + 'TRIGGER_SOURCE' => env['CI_JOB_URL'], + 'TOP_UPSTREAM_SOURCE_PROJECT' => env['CI_PROJECT_PATH'], + 'TOP_UPSTREAM_SOURCE_REF' => env['CI_COMMIT_REF_NAME'], + 'TOP_UPSTREAM_SOURCE_JOB' => env['CI_JOB_URL'], + 'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => env['CI_MERGE_REQUEST_PROJECT_ID'], + 'TOP_UPSTREAM_MERGE_REQUEST_IID' => env['CI_MERGE_REQUEST_IID'] + } + end + + it 'includes simple forwarded variables' do + expect(subject.variables).to include(simple_forwarded_variables) + end + + describe "#base_variables" do + context 'when CI_COMMIT_TAG is set' do + before do + stub_env('CI_COMMIT_TAG', 'v1.0') + end + + it 'sets GITLAB_REF_SLUG to CI_COMMIT_REF_NAME' do + expect(subject.variables['GITLAB_REF_SLUG']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + + context 'when CI_COMMIT_TAG is nil' do + before do + stub_env('CI_COMMIT_TAG', nil) + end + + it 'sets GITLAB_REF_SLUG to CI_COMMIT_REF_SLUG' do + expect(subject.variables['GITLAB_REF_SLUG']).to eq(env['CI_COMMIT_REF_SLUG']) + end + end + + context 'when TRIGGERED_USER is set' do + before do + stub_env('TRIGGERED_USER', 'triggered_user') + end + + it 'sets TRIGGERED_USER to triggered_user' do + expect(subject.variables['TRIGGERED_USER']).to eq('triggered_user') + end + end + + context 'when TRIGGERED_USER is not set' do + before do + stub_env('TRIGGERED_USER', nil) + end + + it 'sets TRIGGERED_USER to GITLAB_USER_NAME' do + expect(subject.variables['TRIGGERED_USER']).to eq(env['GITLAB_USER_NAME']) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha') + end + + it 'sets TOP_UPSTREAM_SOURCE_SHA to ci_merge_request_source_branch_sha' do + expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq('ci_merge_request_source_branch_sha') + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '') + end + + it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do + expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA']) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil) + end + + it 'sets TOP_UPSTREAM_SOURCE_SHA to CI_COMMIT_SHA' do + expect(subject.variables['TOP_UPSTREAM_SOURCE_SHA']).to eq(env['CI_COMMIT_SHA']) + end + end + end + + describe "#version_file_variables" do + using RSpec::Parameterized::TableSyntax + + where(:version_file, :version) do + 'GITALY_SERVER_VERSION' | "1" + 'GITLAB_ELASTICSEARCH_INDEXER_VERSION' | "2" + 'GITLAB_KAS_VERSION' | "3" + 'GITLAB_PAGES_VERSION' | "4" + 'GITLAB_SHELL_VERSION' | "5" + 'GITLAB_WORKHORSE_VERSION' | "6" + end + + with_them do + context "when set in ENV" do + before do + stub_env(version_file, version) + end + + it 'includes the version from ENV' do + expect(subject.variables[version_file]).to eq(version) + end + end + + context "when set in a file" do + before do + allow(File).to receive(:read).and_call_original + end + + it 'includes the version from the file' do + expect(File).to receive(:read).with(version_file).and_return(version) + expect(subject.variables[version_file]).to eq(version) + end + end + end + end + end + end + + describe Trigger::Omnibus do + describe '#variables' do + it 'invokes the trigger with expected variables' do + expect(subject.variables).to include( + 'QA_IMAGE' => env['QA_IMAGE'], + 'SKIP_QA_DOCKER' => 'true', + 'ALTERNATIVE_SOURCES' => 'true', + 'CACHE_UPDATE' => env['OMNIBUS_GITLAB_CACHE_UPDATE'], + 'GITLAB_QA_OPTIONS' => env['GITLAB_QA_OPTIONS'], + 'QA_TESTS' => env['QA_TESTS'], + 'ALLURE_JOB_NAME' => env['ALLURE_JOB_NAME'] + ) + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha') + end + + it 'sets GITLAB_VERSION & IMAGE_TAG to ci_merge_request_source_branch_sha' do + expect(subject.variables).to include( + 'GITLAB_VERSION' => 'ci_merge_request_source_branch_sha', + 'IMAGE_TAG' => 'ci_merge_request_source_branch_sha' + ) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '') + end + + it 'sets GITLAB_VERSION & IMAGE_TAG to CI_COMMIT_SHA' do + expect(subject.variables).to include( + 'GITLAB_VERSION' => env['CI_COMMIT_SHA'], + 'IMAGE_TAG' => env['CI_COMMIT_SHA'] + ) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil) + end + + it 'sets GITLAB_VERSION & IMAGE_TAG to CI_COMMIT_SHA' do + expect(subject.variables).to include( + 'GITLAB_VERSION' => env['CI_COMMIT_SHA'], + 'IMAGE_TAG' => env['CI_COMMIT_SHA'] + ) + end + end + + context 'when Trigger.security? is true' do + before do + allow(Trigger).to receive(:security?).and_return(true) + end + + it 'sets SECURITY_SOURCES to true' do + expect(subject.variables['SECURITY_SOURCES']).to eq('true') + end + end + + context 'when Trigger.security? is false' do + before do + allow(Trigger).to receive(:security?).and_return(false) + end + + it 'sets SECURITY_SOURCES to false' do + expect(subject.variables['SECURITY_SOURCES']).to eq('false') + end + end + + context 'when Trigger.ee? is true' do + before do + allow(Trigger).to receive(:ee?).and_return(true) + end + + it 'sets ee to true' do + expect(subject.variables['ee']).to eq('true') + end + end + + context 'when Trigger.ee? is false' do + before do + allow(Trigger).to receive(:ee?).and_return(false) + end + + it 'sets ee to false' do + expect(subject.variables['ee']).to eq('false') + end + end + + context 'when QA_BRANCH is set' do + before do + stub_env('QA_BRANCH', 'qa_branch') + end + + it 'sets QA_BRANCH to qa_branch' do + expect(subject.variables['QA_BRANCH']).to eq('qa_branch') + end + end + end + + describe '.access_token' do + context 'when OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN is set' do + let(:omnibus_gitlab_project_access_token) { 'omnibus_gitlab_project_access_token' } + + before do + stub_env('OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN', omnibus_gitlab_project_access_token) + end + + it 'returns the omnibus-specific access token' do + expect(described_class.access_token).to eq(omnibus_gitlab_project_access_token) + end + end + + context 'when OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN is not set' do + before do + stub_env('OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN', nil) + end + + it 'returns the default access token' do + expect(described_class.access_token).to eq(Trigger::Base.access_token) + end + end + end + + describe '#invoke!' do + let(:downstream_project_path) { 'gitlab-org/build/omnibus-gitlab-mirror' } + let(:ref) { 'master' } + + let(:env) do + super().merge( + 'QA_IMAGE' => 'qa_image', + 'OMNIBUS_GITLAB_PROJECT_ACCESS_TOKEN' => nil, + 'OMNIBUS_GITLAB_CACHE_UPDATE' => 'omnibus_gitlab_cache_update', + 'GITLAB_QA_OPTIONS' => 'gitlab_qa_options', + 'QA_TESTS' => 'qa_tests', + 'ALLURE_JOB_NAME' => 'allure_job_name' + ) + end + + describe '#downstream_project_path' do + context 'when OMNIBUS_PROJECT_PATH is set' do + let(:downstream_project_path) { 'omnibus_project_path' } + + before do + stub_env('OMNIBUS_PROJECT_PATH', downstream_project_path) + end + + it 'triggers the pipeline on the correct project' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + + describe '#ref' do + context 'when OMNIBUS_BRANCH is set' do + let(:ref) { 'omnibus_branch' } + + before do + stub_env('OMNIBUS_BRANCH', ref) + end + + it 'triggers the pipeline on the correct ref' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + + context 'when CI_COMMIT_REF_NAME is a stable branch' do + let(:ref) { '14-10-stable' } + + before do + stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee") + end + + it 'triggers the pipeline on the correct ref' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + end + + describe Trigger::CNG do + describe '#variables' do + it 'does not include redundant variables' do + expect(subject.variables).not_to include('TRIGGER_SOURCE', 'TRIGGERED_USER') + end + + it 'invokes the trigger with expected variables' do + expect(subject.variables).to include('FORCE_RAILS_IMAGE_BUILDS' => 'true') + end + + describe "TRIGGER_BRANCH" do + context 'when CNG_BRANCH is not set' do + it 'sets TRIGGER_BRANCH to master' do + expect(subject.variables['TRIGGER_BRANCH']).to eq('master') + end + end + + context 'when CNG_BRANCH is set' do + let(:ref) { 'cng_branch' } + + before do + stub_env('CNG_BRANCH', ref) + end + + it 'sets TRIGGER_BRANCH to cng_branch' do + expect(subject.variables['TRIGGER_BRANCH']).to eq(ref) + end + end + + context 'when CI_COMMIT_REF_NAME is a stable branch' do + let(:ref) { '14-10-stable' } + + before do + stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee") + end + + it 'sets TRIGGER_BRANCH to the corresponding stable branch' do + expect(subject.variables['TRIGGER_BRANCH']).to eq(ref) + end + end + end + + describe "GITLAB_VERSION" do + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha') + end + + it 'sets GITLAB_VERSION to CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' do + expect(subject.variables['GITLAB_VERSION']).to eq('ci_merge_request_source_branch_sha') + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '') + end + + it 'sets GITLAB_VERSION to CI_COMMIT_SHA' do + expect(subject.variables['GITLAB_VERSION']).to eq(env['CI_COMMIT_SHA']) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil) + end + + it 'sets GITLAB_VERSION to CI_COMMIT_SHA' do + expect(subject.variables['GITLAB_VERSION']).to eq(env['CI_COMMIT_SHA']) + end + end + end + + describe "GITLAB_TAG" do + context 'when CI_COMMIT_TAG is set' do + before do + stub_env('CI_COMMIT_TAG', 'v1.0') + end + + it 'sets GITLAB_TAG to true' do + expect(subject.variables['GITLAB_TAG']).to eq('v1.0') + end + end + + context 'when CI_COMMIT_TAG is nil' do + before do + stub_env('CI_COMMIT_TAG', nil) + end + + it 'sets GITLAB_TAG to nil' do + expect(subject.variables['GITLAB_TAG']).to eq(nil) + end + end + end + + describe "GITLAB_ASSETS_TAG" do + context 'when CI_COMMIT_TAG is set' do + before do + stub_env('CI_COMMIT_TAG', 'v1.0') + end + + it 'sets GITLAB_ASSETS_TAG to CI_COMMIT_REF_NAME' do + expect(subject.variables['GITLAB_ASSETS_TAG']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + + context 'when CI_COMMIT_TAG and CI_MERGE_REQUEST_SOURCE_BRANCH_SHA are nil' do + before do + stub_env('CI_COMMIT_TAG', nil) + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil) + end + + it 'sets GITLAB_ASSETS_TAG to CI_COMMIT_SHA' do + expect(subject.variables['GITLAB_ASSETS_TAG']).to eq(env['CI_COMMIT_SHA']) + end + end + end + + describe "CE_PIPELINE" do + context 'when Trigger.ee? is true' do + before do + allow(Trigger).to receive(:ee?).and_return(true) + end + + it 'sets CE_PIPELINE to nil' do + expect(subject.variables['CE_PIPELINE']).to eq(nil) + end + end + + context 'when Trigger.ee? is false' do + before do + allow(Trigger).to receive(:ee?).and_return(false) + end + + it 'sets CE_PIPELINE to true' do + expect(subject.variables['CE_PIPELINE']).to eq('true') + end + end + end + + describe "EE_PIPELINE" do + context 'when Trigger.ee? is true' do + before do + allow(Trigger).to receive(:ee?).and_return(true) + end + + it 'sets EE_PIPELINE to true' do + expect(subject.variables['EE_PIPELINE']).to eq('true') + end + end + + context 'when Trigger.ee? is false' do + before do + allow(Trigger).to receive(:ee?).and_return(false) + end + + it 'sets EE_PIPELINE to nil' do + expect(subject.variables['EE_PIPELINE']).to eq(nil) + end + end + end + + describe "#version_param_value" do + using RSpec::Parameterized::TableSyntax + + let(:version_file) { 'GITALY_SERVER_VERSION' } + + where(:raw_version, :expected_version) do + "1.2.3" | "v1.2.3" + "1.2.3-rc1" | "v1.2.3-rc1" + "1.2.3-ee" | "v1.2.3-ee" + "1.2.3-rc1-ee" | "v1.2.3-rc1-ee" + end + + with_them do + context "when set in ENV" do + before do + stub_env(version_file, raw_version) + end + + it 'includes the version from ENV' do + expect(subject.variables[version_file]).to eq(expected_version) + end + end + end + end + end + end + + describe Trigger::Docs do + describe '#variables' do + describe "BRANCH_CE" do + before do + stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab-foss') + end + + context 'when CI_PROJECT_PATH is gitlab-org/gitlab-foss' do + it 'sets BRANCH_CE to CI_COMMIT_REF_NAME' do + expect(subject.variables['BRANCH_CE']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + end + + describe "BRANCH_EE" do + before do + stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab') + end + + context 'when CI_PROJECT_PATH is gitlab-org/gitlab' do + it 'sets BRANCH_EE to CI_COMMIT_REF_NAME' do + expect(subject.variables['BRANCH_EE']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + end + + describe "BRANCH_RUNNER" do + before do + stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab-runner') + end + + context 'when CI_PROJECT_PATH is gitlab-org/gitlab-runner' do + it 'sets BRANCH_RUNNER to CI_COMMIT_REF_NAME' do + expect(subject.variables['BRANCH_RUNNER']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + end + + describe "BRANCH_OMNIBUS" do + before do + stub_env('CI_PROJECT_PATH', 'gitlab-org/omnibus-gitlab') + end + + context 'when CI_PROJECT_PATH is gitlab-org/omnibus-gitlab' do + it 'sets BRANCH_OMNIBUS to CI_COMMIT_REF_NAME' do + expect(subject.variables['BRANCH_OMNIBUS']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + end + + describe "BRANCH_CHARTS" do + before do + stub_env('CI_PROJECT_PATH', 'gitlab-org/charts/gitlab') + end + + context 'when CI_PROJECT_PATH is gitlab-org/charts/gitlab' do + it 'sets BRANCH_CHARTS to CI_COMMIT_REF_NAME' do + expect(subject.variables['BRANCH_CHARTS']).to eq(env['CI_COMMIT_REF_NAME']) + end + end + end + + describe "REVIEW_SLUG" do + before do + stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab-foss') + end + + context 'when CI_MERGE_REQUEST_IID is set' do + it 'sets REVIEW_SLUG' do + expect(subject.variables['REVIEW_SLUG']).to eq("-ce-#{env['CI_MERGE_REQUEST_IID']}") + end + end + + context 'when CI_MERGE_REQUEST_IID is not set' do + before do + stub_env('CI_MERGE_REQUEST_IID', nil) + end + + it 'sets REVIEW_SLUG' do + expect(subject.variables['REVIEW_SLUG']).to eq("-ce-#{env['CI_COMMIT_REF_SLUG']}") + end + end + end + end + + describe '.access_token' do + context 'when DOCS_PROJECT_API_TOKEN is set' do + let(:docs_project_api_token) { 'docs_project_api_token' } + + before do + stub_env('DOCS_PROJECT_API_TOKEN', docs_project_api_token) + end + + it 'returns the docs-specific access token' do + expect(described_class.access_token).to eq(docs_project_api_token) + end + end + + context 'when DOCS_PROJECT_API_TOKEN is not set' do + before do + stub_env('DOCS_PROJECT_API_TOKEN', nil) + end + + it 'returns the default access token' do + expect(described_class.access_token).to eq(Trigger::Base.access_token) + end + end + end + + describe '#invoke!' do + let(:downstream_project_path) { 'gitlab-org/gitlab-docs' } + let(:trigger_token) { 'docs_trigger_token' } + let(:ref) { 'main' } + + let(:env) do + super().merge( + 'CI_PROJECT_PATH' => 'gitlab-org/gitlab-foss', + 'DOCS_PROJECT_API_TOKEN' => nil, + 'DOCS_TRIGGER_TOKEN' => trigger_token + ) + end + + describe '#downstream_project_path' do + context 'when DOCS_PROJECT_PATH is set' do + let(:downstream_project_path) { 'docs_project_path' } + + before do + stub_env('DOCS_PROJECT_PATH', downstream_project_path) + end + + it 'triggers the pipeline on the correct project' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + + describe '#ref' do + context 'when DOCS_BRANCH is set' do + let(:ref) { 'docs_branch' } + + before do + stub_env('DOCS_BRANCH', ref) + end + + it 'triggers the pipeline on the correct ref' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + end + end + + describe Trigger::DatabaseTesting do + describe '#variables' do + it 'invokes the trigger with expected variables' do + expect(subject.variables).to include('TRIGGERED_USER_LOGIN' => env['GITLAB_USER_LOGIN']) + end + + describe "GITLAB_COMMIT_SHA" do + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', 'ci_merge_request_source_branch_sha') + end + + it 'sets GITLAB_COMMIT_SHA to ci_merge_request_source_branch_sha' do + expect(subject.variables['GITLAB_COMMIT_SHA']).to eq('ci_merge_request_source_branch_sha') + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is set as empty' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '') + end + + it 'sets GITLAB_COMMIT_SHA to CI_COMMIT_SHA' do + expect(subject.variables['GITLAB_COMMIT_SHA']).to eq(env['CI_COMMIT_SHA']) + end + end + + context 'when CI_MERGE_REQUEST_SOURCE_BRANCH_SHA is not set' do + before do + stub_env('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', nil) + end + + it 'sets GITLAB_COMMIT_SHA to CI_COMMIT_SHA' do + expect(subject.variables['GITLAB_COMMIT_SHA']).to eq(env['CI_COMMIT_SHA']) + end + end + end + end + + describe '#invoke!' do + let(:downstream_project_path) { 'gitlab-com/database-team/gitlab-com-database-testing' } + let(:trigger_token) { 'gitlabcom_database_testing_access_token' } + let(:api_endpoint) { 'https://ops.gitlab.net/api/v4' } + let(:gitlab_client_private_token) { 'gitlabcom_database_testing_access_token' } + let(:ref) { 'master' } + let(:stubbed_upstream_gitlab_client) { double('upstream_gitlab_api_client') } + let(:mr_notes) { [double(body: described_class::IDENTIFIABLE_NOTE_TAG)] } + + let(:env) do + super().merge( + 'GITLABCOM_DATABASE_TESTING_ACCESS_TOKEN' => gitlab_client_private_token, + 'GITLABCOM_DATABASE_TESTING_TRIGGER_TOKEN' => trigger_token + ) + end + + before do + allow(Gitlab).to receive(:client) + .with( + endpoint: 'https://gitlab.com/api/v4', + private_token: env['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] + ) + .and_return(stubbed_upstream_gitlab_client) + allow(stubbed_upstream_gitlab_client).to receive(:merge_request_notes) + .with( + env['CI_PROJECT_PATH'], + env['CI_MERGE_REQUEST_IID'] + ) + .and_return(double(auto_paginate: mr_notes)) + end + + it 'invokes the trigger with expected variables' do + expect_run_trigger_with_params + + subject.invoke! + end + + describe '#downstream_project_path' do + context 'when GITLABCOM_DATABASE_TESTING_PROJECT_PATH is set' do + let(:downstream_project_path) { 'gitlabcom_database_testing_project_path' } + + before do + stub_env('GITLABCOM_DATABASE_TESTING_PROJECT_PATH', downstream_project_path) + end + + it 'triggers the pipeline on the correct project' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + + describe '#ref' do + context 'when GITLABCOM_DATABASE_TESTING_TRIGGER_REF is set' do + let(:ref) { 'gitlabcom_database_testing_trigger_ref' } + + before do + stub_env('GITLABCOM_DATABASE_TESTING_TRIGGER_REF', ref) + end + + it 'triggers the pipeline on the correct ref' do + expect_run_trigger_with_params + + subject.invoke! + end + end + end + + context 'when no MR notes with the identifier exist yet' do + let(:mr_notes) { [double(body: 'hello world')] } + + it 'posts a new note' do + expect_run_trigger_with_params + expect(stubbed_upstream_gitlab_client).to receive(:create_merge_request_note) + .with( + env['CI_PROJECT_PATH'], + env['CI_MERGE_REQUEST_IID'], + instance_of(String) + ) + .and_return(double(id: 42)) + + subject.invoke! + end + end + end + end +end +# rubocop:enable RSpec/VerifiedDoubles