diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-08 15:07:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-08 15:07:09 +0300 |
commit | e44c3e4832e43c77e9c29fad6e49f8d6066d7f5c (patch) | |
tree | 892f4505093dd5ffd60e238a8b74b35f021654ae | |
parent | aa1c2a29b8ddc82141f826eacd169d3d7ff66611 (diff) |
Add latest changes from gitlab-org/gitlab@master
53 files changed, 696 insertions, 399 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 6c3bb88f439..0b0636a9b07 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -14.23.0 +14.26.0 diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue index dd80698ec1a..509bdabdd9e 100644 --- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue +++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue @@ -2,6 +2,7 @@ import { GlAlert, GlButton, + GlCard, GlIcon, GlLoadingIcon, GlModal, @@ -24,6 +25,7 @@ export default { components: { GlAlert, GlButton, + GlCard, GlIcon, GlLoadingIcon, GlModal, @@ -42,6 +44,7 @@ export default { inject: ['projectId', 'admin', 'fileSizeLimit'], DEFAULT_PER_PAGE, i18n: { + title: __('Files'), deleteLabel: __('Delete File'), uploadLabel: __('Upload File'), uploadingLabel: __('Uploading...'), @@ -89,7 +92,8 @@ export default { }, { key: 'actions', - label: '', + label: __('Actions'), + thClass: 'gl-text-right', tdClass: 'gl-text-right gl-vertical-align-middle!', }, ], @@ -184,78 +188,95 @@ export default { <template> <div> - <div class="ci-secure-files-table"> + <div> <gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="error = null"> {{ errorMessage }} </gl-alert> - <gl-table - :busy="loading" - :fields="fields" - :items="projectSecureFiles" - tbody-tr-class="js-ci-secure-files-row" - data-qa-selector="ci_secure_files_table_content" - sort-by="key" - sort-direction="asc" - stacked="lg" - table-class="text-secondary" - show-empty - sort-icon-left - no-sort-reset - :empty-text="$options.i18n.noFilesMessage" + <gl-card + class="gl-new-card" + header-class="gl-new-card-header" + body-class="gl-new-card-body gl-px-0" > - <template #table-busy> - <gl-loading-icon size="lg" class="gl-my-5" /> + <template #header> + <div class="gl-new-card-title-wrapper"> + <h5 class="gl-new-card-title gl-my-0"> + {{ $options.i18n.title }} + <span class="gl-new-card-count"> + <gl-icon name="document" class="gl-mr-2" /> + {{ projectSecureFiles.length }} + </span> + </h5> + </div> + <div class="gl-new-card-actions"> + <gl-button v-if="admin" size="small" @click="loadFileSelector"> + <span v-if="uploading"> + <gl-loading-icon class="gl-my-5" inline /> + {{ $options.i18n.uploadingLabel }} + </span> + <span v-else> + <gl-icon name="upload" class="gl-mr-2" /> {{ $options.i18n.uploadLabel }} + </span> + </gl-button> + <input + id="file-upload" + ref="fileUpload" + type="file" + class="hidden" + data-qa-selector="file_upload_field" + @change="uploadSecureFile" + /> + </div> </template> - <template #cell(name)="{ item }"> - {{ item.name }} - </template> + <gl-table + :busy="loading" + :fields="fields" + :items="projectSecureFiles" + tbody-tr-class="js-ci-secure-files-row" + sort-by="key" + sort-direction="asc" + stacked="md" + table-class="text-secondary" + show-empty + sort-icon-left + no-sort-reset + :empty-text="$options.i18n.noFilesMessage" + > + <template #table-busy> + <gl-loading-icon size="lg" class="gl-my-5" /> + </template> - <template #cell(created_at)="{ item }"> - <timeago-tooltip :time="item.created_at" /> - </template> + <template #cell(name)="{ item }"> + {{ item.name }} + </template> - <template #cell(actions)="{ item }"> - <metadata-button - :secure-file="item" - :admin="admin" - modal-id="$options.metadataModalId" - @selectSecureFile="updateMetadataSecureFile" - /> - <gl-button - v-if="admin" - v-gl-modal="$options.deleteModalId" - v-gl-tooltip.hover.top="$options.i18n.deleteLabel" - category="secondary" - variant="danger" - icon="remove" - :aria-label="$options.i18n.deleteLabel" - data-testid="delete-button" - @click="setDeleteModalData(item)" - /> - </template> - </gl-table> - </div> + <template #cell(created_at)="{ item }"> + <timeago-tooltip :time="item.created_at" /> + </template> - <div class="gl-display-flex gl-mt-5"> - <gl-button v-if="admin" variant="confirm" @click="loadFileSelector"> - <span v-if="uploading"> - <gl-loading-icon class="gl-my-5" inline /> - {{ $options.i18n.uploadingLabel }} - </span> - <span v-else> - <gl-icon name="upload" class="gl-mr-2" /> {{ $options.i18n.uploadLabel }} - </span> - </gl-button> - <input - id="file-upload" - ref="fileUpload" - type="file" - class="hidden" - data-qa-selector="file_upload_field" - @change="uploadSecureFile" - /> + <template #cell(actions)="{ item }"> + <metadata-button + :secure-file="item" + :admin="admin" + modal-id="$options.metadataModalId" + @selectSecureFile="updateMetadataSecureFile" + /> + <gl-button + v-if="admin" + v-gl-modal="$options.deleteModalId" + v-gl-tooltip.hover.top="$options.i18n.deleteLabel" + size="small" + category="tertiary" + variant="default" + icon="remove" + :aria-label="$options.i18n.deleteLabel" + data-testid="delete-button" + @click="setDeleteModalData(item)" + /> + </template> + </gl-table> + </gl-card> </div> <gl-pagination @@ -266,6 +287,7 @@ export default { :next-text="$options.i18n.pagination.next" :prev-text="$options.i18n.pagination.prev" align="center" + class="gl-mt-5" /> <gl-modal diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue index c5f3410a68f..439280eebd4 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue @@ -64,11 +64,8 @@ export default { ProjectsList, GroupsList, }, + inject: ['contextSwitcherLinks'], props: { - persistentLinks: { - type: Array, - required: true, - }, username: { type: String, required: true, @@ -185,7 +182,7 @@ export default { class="gl-border-b gl-border-gray-50 gl-px-0 gl-py-2" > <nav-item - v-for="item in persistentLinks" + v-for="item in contextSwitcherLinks" :key="item.link" :item="item" :link-classes="{ [item.link_classes]: item.link_classes }" diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue index d3bc8f44725..0f2ac5ab03f 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue @@ -361,12 +361,11 @@ export default { @updated="highlightFirstCommand" /> + <global-search-default-items v-else-if="showDefaultItems" /> + <template v-else> - <global-search-default-items v-if="showDefaultItems" /> - <template v-else> - <global-search-scoped-items v-if="showScopedSearchItems" /> - <global-search-autocomplete-items /> - </template> + <global-search-scoped-items v-if="showScopedSearchItems" /> + <global-search-autocomplete-items /> </template> </div> <template v-if="searchContext"> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue index 239c61fd750..eb76277d12b 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue +++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_items.vue @@ -1,31 +1,41 @@ <script> import { GlDisclosureDropdownGroup } from '@gitlab/ui'; import { mapState, mapGetters } from 'vuex'; -import { ALL_GITLAB } from '~/vue_shared/global_search/constants'; +import { ALL_GITLAB, PLACES } from '~/vue_shared/global_search/constants'; export default { name: 'GlobalSearchDefaultItems', i18n: { ALL_GITLAB, + PLACES, }, components: { GlDisclosureDropdownGroup, }, + inject: ['contextSwitcherLinks'], computed: { ...mapState(['searchContext']), ...mapGetters(['defaultSearchOptions']), - sectionHeader() { + currentContextName() { return ( this.searchContext?.project?.name || this.searchContext?.group?.name || this.$options.i18n.ALL_GITLAB ); }, - defaultItemsGroup() { - return { - name: this.sectionHeader, - items: this.defaultSearchOptions, - }; + groups() { + const groups = [ + { + name: this.$options.i18n.PLACES, + items: this.contextSwitcherLinks.map(({ title, link }) => ({ text: title, href: link })), + }, + { + name: this.currentContextName, + items: this.defaultSearchOptions, + }, + ]; + + return groups.filter(({ items }) => items.length > 0); }, }, }; @@ -33,6 +43,12 @@ export default { <template> <ul class="gl-p-0 gl-m-0 gl-list-style-none"> - <gl-disclosure-dropdown-group :group="defaultItemsGroup" bordered class="gl-mt-0!" /> + <gl-disclosure-dropdown-group + v-for="(group, index) of groups" + :key="group.name" + :group="group" + bordered + :class="{ 'gl-mt-0!': index === 0 }" + /> </ul> </template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js index 89bd41ea6c4..6871dabc9a1 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js +++ b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js @@ -48,7 +48,7 @@ export const scopedIssuesPath = (state) => { return ( state.searchContext?.project_metadata?.issues_path || state.searchContext?.group_metadata?.issues_path || - state.issuesPath + (gon.current_username ? state.issuesPath : false) ); }; @@ -56,7 +56,7 @@ export const scopedMRPath = (state) => { return ( state.searchContext?.project_metadata?.mr_path || state.searchContext?.group_metadata?.mr_path || - state.mrPath + (gon.current_username ? state.mrPath : false) ); }; @@ -64,16 +64,23 @@ export const defaultSearchOptions = (state, getters) => { const userName = gon.current_username; if (!userName) { - return [ - { + const options = []; + + if (getters.scopedIssuesPath) { + options.push({ text: ISSUES_CATEGORY, href: getters.scopedIssuesPath, - }, - { + }); + } + + if (getters.scopedMRPath) { + options.push({ text: MERGE_REQUEST_CATEGORY, href: getters.scopedMRPath, - }, - ]; + }); + } + + return options; } const issues = [ diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index dd07d2f21e0..4fc5ea62e95 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -140,7 +140,6 @@ export default { <context-switcher v-if="sidebarData.is_logged_in" ref="context-switcher" - :persistent-links="sidebarData.context_switcher_links" :username="sidebarData.username" :projects-path="sidebarData.projects_path" :groups-path="sidebarData.groups_path" diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index 322eca72016..8a8b3998652 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -84,6 +84,7 @@ export const initSuperSidebar = () => { const projectBlobPath = commandPaletteData.project_blob_url; const commandPaletteCommands = sidebarData.create_new_menu_groups || []; const commandPaletteLinks = convertObjectPropsToCamelCase(sidebarData.current_menu_items || []); + const contextSwitcherLinks = sidebarData.context_switcher_links; const { searchPath, issuesPath, mrPath, autocompletePath, searchContext } = searchData; const isImpersonating = parseBoolean(sidebarData.is_impersonating); @@ -99,6 +100,7 @@ export const initSuperSidebar = () => { ...getTrialStatusWidgetData(sidebarData), commandPaletteCommands, commandPaletteLinks, + contextSwitcherLinks, autocompletePath, searchContext, projectFilesPath, diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js index a693d4f114d..43110c0c9af 100644 --- a/app/assets/javascripts/vue_shared/global_search/constants.js +++ b/app/assets/javascripts/vue_shared/global_search/constants.js @@ -6,6 +6,7 @@ export const AUTOCOMPLETE_ERROR_MESSAGE = s__( export const ALL_GITLAB = __('All GitLab'); export const SEARCH_GITLAB = s__('GlobalSearch|Search GitLab'); +export const PLACES = s__('GlobalSearch|Places'); export const SEARCH_DESCRIBED_BY_DEFAULT = s__( 'GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list.', diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 8e8f0d52f0a..6748d640390 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -50,8 +50,7 @@ } } -.deploy-freeze-table, -.ci-secure-files-table { +.deploy-freeze-table { table { tr { td, diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index cb340675b72..e4301e5afdb 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -76,7 +76,6 @@ module Types description: 'Runner\'s maintenance notes.' field :managers, ::Types::Ci::RunnerManagerType.connection_type, null: true, description: 'Machines associated with the runner configuration.', - method: :runner_managers, alpha: { milestone: '15.10' } field :maximum_timeout, GraphQL::Types::Int, null: true, description: 'Maximum timeout (in seconds) for jobs processed by the runner.' @@ -173,6 +172,18 @@ module Types end end + def managers + BatchLoader::GraphQL.for(runner.id).batch(key: :runner_managers) do |runner_ids, loader| + runner_managers_by_runner_id = + ::Ci::RunnerManager.for_runner(runner_ids).order_id_desc.group_by(&:runner_id) + + runner_ids.each do |runner_id| + runner_managers = Array.wrap(runner_managers_by_runner_id[runner_id]) + loader.call(runner_id, runner_managers) + end + end + end + def job_execution_status BatchLoader::GraphQL.for(runner.id).batch(key: :running_builds_exist) do |runner_ids, loader| statuses = ::Ci::Runner.id_in(runner_ids).with_running_builds.index_by(&:id) diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 35363d07051..1bd7da0a352 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -53,6 +53,7 @@ module SidebarsHelper def super_sidebar_logged_out_context(panel:, panel_type:) # rubocop:disable Metrics/AbcSize { is_logged_in: false, + context_switcher_links: context_switcher_links, current_menu_items: panel.super_sidebar_menu_items, current_context_header: panel.super_sidebar_context_header, support_path: support_url, @@ -101,7 +102,6 @@ module SidebarsHelper gitlab_com_and_canary: Gitlab.com_and_canary?, canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url, current_context: super_sidebar_current_context(project: project, group: group), - context_switcher_links: context_switcher_links, pinned_items: user.pinned_nav_items[panel_type] || super_sidebar_default_pins(panel_type), update_pins_url: pins_path, is_impersonating: impersonating?, @@ -344,8 +344,7 @@ module SidebarsHelper def context_switcher_links links = [ - # We should probably not return "You work" when used is not logged-in - { title: s_('Navigation|Your work'), link: root_path, icon: 'work' }, + ({ title: s_('Navigation|Your work'), link: root_path, icon: 'work' } if current_user), { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' } ] @@ -381,7 +380,7 @@ module SidebarsHelper end # rubocop: enable Cop/UserAdmin - links + links.compact end def impersonating? diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb index 1c06c786b9d..eebba968f54 100644 --- a/app/models/ci/runner_manager.rb +++ b/app/models/ci/runner_manager.rb @@ -49,6 +49,8 @@ module Ci where(runner_id: runner_id) end + scope :order_id_desc, -> { order(id: :desc) } + def self.online_contact_time_deadline Ci::Runner.online_contact_time_deadline end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 5cd85fcf5ee..3a498972153 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -154,7 +154,7 @@ module Ci end def manual_playable? - blocked? + blocked? || skipped? end # This will be removed with ci_remove_ensure_stage_service diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index d0ce6a2c846..703f057d9a5 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -118,7 +118,7 @@ = _("Secure Files") = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do = expanded ? _('Collapse') : _('Expand') - %p + %p.gl-text-secondary.gl-mb-0 = _("Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.") = link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer' .settings-content diff --git a/db/docs/target_branch_rules.yml b/db/docs/target_branch_rules.yml new file mode 100644 index 00000000000..a2bd0795ab2 --- /dev/null +++ b/db/docs/target_branch_rules.yml @@ -0,0 +1,10 @@ +--- +table_name: target_branch_rules +classes: +- Projects::TargetBranchRule +feature_categories: +- code_review_workflow +description: Represents a target branch rule +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126877 +milestone: "16.3" +gitlab_schema: gitlab_main diff --git a/db/migrate/20230718145747_create_target_branch_rules.rb b/db/migrate/20230718145747_create_target_branch_rules.rb new file mode 100644 index 00000000000..e9887a6c49f --- /dev/null +++ b/db/migrate/20230718145747_create_target_branch_rules.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateTargetBranchRules < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + create_table :target_branch_rules do |t| + t.timestamps_with_timezone null: false + t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false + t.text :name, null: false, limit: 255 + # rubocop:disable Migration/AddLimitToTextColumns + # Branch names can be long so we allow for a large number of characters + t.text :target_branch, null: false + # rubocop:enable Migration/AddLimitToTextColumns + end + end +end diff --git a/db/migrate/20230731121354_remove_not_null_from_subscription_add_on_purchases_namespace_id.rb b/db/migrate/20230731121354_remove_not_null_from_subscription_add_on_purchases_namespace_id.rb new file mode 100644 index 00000000000..1de02e0f4ed --- /dev/null +++ b/db/migrate/20230731121354_remove_not_null_from_subscription_add_on_purchases_namespace_id.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemoveNotNullFromSubscriptionAddOnPurchasesNamespaceId < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + change_column_null :subscription_add_on_purchases, :namespace_id, true + end + + def down + change_column_null :subscription_add_on_purchases, :namespace_id, false + end +end diff --git a/db/post_migrate/20230731210422_remove_temp_index_vulnerability_occurrences.rb b/db/post_migrate/20230731210422_remove_temp_index_vulnerability_occurrences.rb new file mode 100644 index 00000000000..a4be163cc64 --- /dev/null +++ b/db/post_migrate/20230731210422_remove_temp_index_vulnerability_occurrences.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class RemoveTempIndexVulnerabilityOccurrences < Gitlab::Database::Migration[2.1] + INDEX_NAME = 'tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99' + + disable_ddl_transaction! + + def up + remove_concurrent_index_by_name :vulnerability_occurrences, INDEX_NAME + end + + def down + add_concurrent_index :vulnerability_occurrences, :id, where: 'report_type IN (7, 99)', name: INDEX_NAME + end +end diff --git a/db/post_migrate/20230807085752_ensure_id_uniqueness_for_p_ci_builds.rb b/db/post_migrate/20230807085752_ensure_id_uniqueness_for_p_ci_builds.rb new file mode 100644 index 00000000000..cdd68e7f5dc --- /dev/null +++ b/db/post_migrate/20230807085752_ensure_id_uniqueness_for_p_ci_builds.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class EnsureIdUniquenessForPCiBuilds < Gitlab::Database::Migration[2.1] + include Gitlab::Database::SchemaHelpers + include Gitlab::Database::MigrationHelpers::WraparoundAutovacuum + + enable_lock_retries! + + TABLE_NAME = :p_ci_builds + FUNCTION_NAME = :assign_p_ci_builds_id_value + TRIGGER_NAME = :assign_p_ci_builds_id_trigger + + def up + return unless should_run? + + change_column_default(TABLE_NAME, :id, nil) + + create_trigger_function(FUNCTION_NAME) do + <<~SQL + IF NEW."id" IS NOT NULL THEN + RAISE WARNING 'Manually assigning ids is not allowed, the value will be ignored'; + END IF; + NEW."id" := nextval('ci_builds_id_seq'::regclass); + RETURN NEW; + SQL + end + + Gitlab::Database::PostgresPartitionedTable.each_partition(TABLE_NAME) do |partition| + create_trigger(partition.identifier, TRIGGER_NAME, FUNCTION_NAME, fires: 'BEFORE INSERT') + end + end + + def down + return unless should_run? + + execute(<<~SQL.squish) + ALTER TABLE #{TABLE_NAME} + ALTER COLUMN id SET DEFAULT nextval('ci_builds_id_seq'::regclass); + + DROP FUNCTION IF EXISTS #{FUNCTION_NAME} CASCADE; + SQL + end + + private + + def should_run? + can_execute_on?(:ci_builds) + end +end diff --git a/db/schema_migrations/20230718145747 b/db/schema_migrations/20230718145747 new file mode 100644 index 00000000000..228326dc30d --- /dev/null +++ b/db/schema_migrations/20230718145747 @@ -0,0 +1 @@ +5bd873b112743d11011ceebb610a1ce3353e46f5652f7413d9c93d468c56f90d
\ No newline at end of file diff --git a/db/schema_migrations/20230731121354 b/db/schema_migrations/20230731121354 new file mode 100644 index 00000000000..c5770298707 --- /dev/null +++ b/db/schema_migrations/20230731121354 @@ -0,0 +1 @@ +e04e8910fdcf1f15522b80248771d3c3e6499eb64d233e29ed83ce0577cb3a98
\ No newline at end of file diff --git a/db/schema_migrations/20230731210422 b/db/schema_migrations/20230731210422 new file mode 100644 index 00000000000..10689aa1fba --- /dev/null +++ b/db/schema_migrations/20230731210422 @@ -0,0 +1 @@ +288e4e5aa1a26878372d79d562fa8d6b683394081ec4270152eaac6d99df2bac
\ No newline at end of file diff --git a/db/schema_migrations/20230807085752 b/db/schema_migrations/20230807085752 new file mode 100644 index 00000000000..ae2b041fc7c --- /dev/null +++ b/db/schema_migrations/20230807085752 @@ -0,0 +1 @@ +5bcb3c38cad89f3f06c7583bbdda8d7bdf48d987ce57fb14cac606191380b392
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3d58a54933b..531e02b43fb 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10,6 +10,19 @@ CREATE EXTENSION IF NOT EXISTS btree_gist; CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE FUNCTION assign_p_ci_builds_id_value() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN +IF NEW."id" IS NOT NULL THEN + RAISE WARNING 'Manually assigning ids is not allowed, the value will be ignored'; +END IF; +NEW."id" := nextval('ci_builds_id_seq'::regclass); +RETURN NEW; + +END +$$; + CREATE FUNCTION delete_associated_project_namespace() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -13225,15 +13238,6 @@ CREATE TABLE p_ci_builds ( ) PARTITION BY LIST (partition_id); -CREATE SEQUENCE ci_builds_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - -ALTER SEQUENCE ci_builds_id_seq OWNED BY p_ci_builds.id; - CREATE TABLE ci_builds ( status character varying, finished_at timestamp without time zone, @@ -13276,13 +13280,22 @@ CREATE TABLE ci_builds ( waiting_for_resource_at timestamp with time zone, processed boolean, scheduling_type smallint, - id bigint DEFAULT nextval('ci_builds_id_seq'::regclass) NOT NULL, + id bigint NOT NULL, stage_id bigint, partition_id bigint NOT NULL, CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL)) ); ALTER TABLE ONLY p_ci_builds ATTACH PARTITION ci_builds FOR VALUES IN ('100'); +CREATE SEQUENCE ci_builds_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE ci_builds_id_seq OWNED BY p_ci_builds.id; + CREATE TABLE p_ci_builds_metadata ( project_id integer NOT NULL, timeout integer, @@ -23249,7 +23262,7 @@ CREATE TABLE subscription_add_on_purchases ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, subscription_add_on_id bigint NOT NULL, - namespace_id bigint NOT NULL, + namespace_id bigint, quantity integer NOT NULL, expires_on date NOT NULL, purchase_xid text NOT NULL, @@ -23442,6 +23455,25 @@ CREATE SEQUENCE tags_id_seq ALTER SEQUENCE tags_id_seq OWNED BY tags.id; +CREATE TABLE target_branch_rules ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + project_id bigint NOT NULL, + name text NOT NULL, + target_branch text NOT NULL, + CONSTRAINT check_3a0b12cf8c CHECK ((char_length(name) <= 255)) +); + +CREATE SEQUENCE target_branch_rules_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE target_branch_rules_id_seq OWNED BY target_branch_rules.id; + CREATE TABLE term_agreements ( id integer NOT NULL, term_id integer NOT NULL, @@ -25828,8 +25860,6 @@ ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organization ALTER TABLE ONLY p_batched_git_ref_updates_deletions ALTER COLUMN id SET DEFAULT nextval('p_batched_git_ref_updates_deletions_id_seq'::regclass); -ALTER TABLE ONLY p_ci_builds ALTER COLUMN id SET DEFAULT nextval('ci_builds_id_seq'::regclass); - ALTER TABLE ONLY p_ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('ci_builds_metadata_id_seq'::regclass); ALTER TABLE ONLY p_ci_job_annotations ALTER COLUMN id SET DEFAULT nextval('p_ci_job_annotations_id_seq'::regclass); @@ -26142,6 +26172,8 @@ ALTER TABLE ONLY taggings ALTER COLUMN id SET DEFAULT nextval('taggings_id_seq': ALTER TABLE ONLY tags ALTER COLUMN id SET DEFAULT nextval('tags_id_seq'::regclass); +ALTER TABLE ONLY target_branch_rules ALTER COLUMN id SET DEFAULT nextval('target_branch_rules_id_seq'::regclass); + ALTER TABLE ONLY term_agreements ALTER COLUMN id SET DEFAULT nextval('term_agreements_id_seq'::regclass); ALTER TABLE ONLY terraform_state_versions ALTER COLUMN id SET DEFAULT nextval('terraform_state_versions_id_seq'::regclass); @@ -28661,6 +28693,9 @@ ALTER TABLE ONLY taggings ALTER TABLE ONLY tags ADD CONSTRAINT tags_pkey PRIMARY KEY (id); +ALTER TABLE ONLY target_branch_rules + ADD CONSTRAINT target_branch_rules_pkey PRIMARY KEY (id); + ALTER TABLE ONLY term_agreements ADD CONSTRAINT term_agreements_pkey PRIMARY KEY (id); @@ -33410,6 +33445,8 @@ CREATE UNIQUE INDEX index_tags_on_name ON tags USING btree (name); CREATE INDEX index_tags_on_name_trigram ON tags USING gin (name gin_trgm_ops); +CREATE INDEX index_target_branch_rules_on_project_id ON target_branch_rules USING btree (project_id); + CREATE INDEX index_term_agreements_on_term_id ON term_agreements USING btree (term_id); CREATE INDEX index_term_agreements_on_user_id ON term_agreements USING btree (user_id); @@ -34014,8 +34051,6 @@ CREATE INDEX tmp_idx_packages_on_project_id_when_npm_not_pending_destruction ON CREATE INDEX tmp_idx_vuln_reads_where_dismissal_reason_null ON vulnerability_reads USING btree (id) WHERE ((state = 2) AND (dismissal_reason IS NULL)); -CREATE INDEX tmp_idx_vulnerability_occurrences_on_id_where_report_type_7_99 ON vulnerability_occurrences USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99])); - CREATE INDEX tmp_idx_vulns_on_converted_uuid ON vulnerability_occurrences USING btree (id, uuid_convert_string_to_uuid) WHERE (uuid_convert_string_to_uuid = '00000000-0000-0000-0000-000000000000'::uuid); CREATE INDEX tmp_index_ci_job_artifacts_on_expire_at_where_locked_unknown ON ci_job_artifacts USING btree (expire_at, job_id) WHERE ((locked = 2) AND (expire_at IS NOT NULL)); @@ -35706,6 +35741,8 @@ ALTER INDEX p_ci_builds_scheduled_at_idx ATTACH PARTITION partial_index_ci_build ALTER INDEX p_ci_builds_token_encrypted_partition_id_idx ATTACH PARTITION unique_ci_builds_token_encrypted_and_partition_id; +CREATE TRIGGER assign_p_ci_builds_id_trigger BEFORE INSERT ON ci_builds FOR EACH ROW EXECUTE FUNCTION assign_p_ci_builds_id_value(); + CREATE TRIGGER chat_names_loose_fk_trigger AFTER DELETE ON chat_names REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); CREATE TRIGGER ci_builds_loose_fk_trigger AFTER DELETE ON ci_builds REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); @@ -37969,6 +38006,9 @@ ALTER TABLE ONLY badges ALTER TABLE ONLY vulnerability_finding_signatures ADD CONSTRAINT fk_rails_9e0baf9dcd FOREIGN KEY (finding_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE; +ALTER TABLE ONLY target_branch_rules + ADD CONSTRAINT fk_rails_9e9cf81c8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY timelog_categories ADD CONSTRAINT fk_rails_9f27b821a8 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/administration/settings/sign_up_restrictions.md b/doc/administration/settings/sign_up_restrictions.md index 719f205ba32..f255e15c1be 100644 --- a/doc/administration/settings/sign_up_restrictions.md +++ b/doc/administration/settings/sign_up_restrictions.md @@ -64,7 +64,7 @@ A [user cap](#user-cap) can also be used to enforce approvals for new users. You can send confirmation emails during sign up and require that users confirm their email address before they are allowed to sign in. -For example, to enforce confirmation of the email address used for new sign ups: +To enforce confirmation of the email address used for new sign ups: 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. Select **Admin Area**. @@ -75,7 +75,7 @@ For example, to enforce confirmation of the email address used for new sign ups: The following settings are available: - **Hard** - Send a confirmation email during sign up. New users must confirm their email address before they can log in. -- **Soft** - Send a confirmation email during sign up. New users can log in immediately, but must confirm their email in three days. Unconfirmed accounts are deleted. +- **Soft** - Send a confirmation email during sign up. New users can sign in immediately, but must confirm their email in three days. After three days, the user is not able to sign in until they confirm their email. - **Off** - New users can sign up without confirming their email address. ## User cap diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index bae324a1493..4bd60433180 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -15922,12 +15922,22 @@ Representing an event. | <a id="eventid"></a>`id` | [`ID!`](#id) | ID of the event. | | <a id="eventupdatedat"></a>`updatedAt` | [`Time!`](#time) | When this event was updated. | +### `ExplainVulnerabilityPresubmissionCheckResults` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="explainvulnerabilitypresubmissioncheckresultspotentialsecretsincode"></a>`potentialSecretsInCode` | [`Boolean!`](#boolean) | This flag is true if we think there might be a secret in the code that would be sent in the LLM prompt. | +| <a id="explainvulnerabilitypresubmissioncheckresultssecretdetectionresult"></a>`secretDetectionResult` | [`Boolean!`](#boolean) | This flag is true if the vulnerability being explained is specifically a secret detection vulnerability. | + ### `ExplainVulnerabilityPrompt` #### Fields | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="explainvulnerabilitypromptpresubmissionchecks"></a>`presubmissionChecks` | [`ExplainVulnerabilityPresubmissionCheckResults!`](#explainvulnerabilitypresubmissioncheckresults) | An object containing booleans. Each booolean indicates the result of a presubmission check: `true` for passed, and `false` for failed. | | <a id="explainvulnerabilitypromptpromptwithcode"></a>`promptWithCode` | [`String`](#string) | AI text prompt generated using the vulnerability's information, including the vulnerable code. | | <a id="explainvulnerabilitypromptpromptwithoutcode"></a>`promptWithoutCode` | [`String`](#string) | AI text prompt generated using the vulnerability's information, excluding the vulnerable code. | diff --git a/doc/development/fips_compliance.md b/doc/development/fips_compliance.md index bab4d7705f9..347cf561e24 100644 --- a/doc/development/fips_compliance.md +++ b/doc/development/fips_compliance.md @@ -118,48 +118,8 @@ for more details. The following instructions build on the Quick Start and are al ##### Terraform: Use a FIPS AMI -1. Follow the guide to set up Terraform and Ansible. -1. After [step 2b](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/docs/environment_quick_start_guide.md#2b-setup-config), - create a `data.tf` in your environment (for example, `gitlab-environment-toolkit/terraform/environments/gitlab-10k/inventory/data.tf`): - - ```tf - data "aws_ami" "ubuntu_20_04_fips" { - count = 1 - - most_recent = true - - filter { - name = "name" - values = ["ubuntu-pro-fips-server/images/hvm-ssd/ubuntu-focal-20.04-amd64-pro-fips-server-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - owners = ["aws-marketplace"] - } - ``` - -1. Add the custom `ami_id` to use this AMI in `environment.tf`. For - example, in `gitlab-environment-toolkit/terraform/environments/gitlab-10k/inventory/environment.tf`: - - ```tf - module "gitlab_ref_arch_aws" { - source = "../../modules/gitlab_ref_arch_aws" - - prefix = var.prefix - ami_id = data.aws_ami.ubuntu_20_04_fips[0].id - ... - ``` - -NOTE: -GET does not allow the AMI to change on EC2 instances after it has -been deployed via `terraform apply`. Since an AMI change would tear down -an instance, this would result in data loss: not only would disks be -destroyed, but also GitLab secrets would be lost. There is a [Terraform lifecycle rule](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/blob/2aaeaff8ac8067f23cd7b6bb5bf131061649089d/terraform/modules/gitlab_aws_instance/main.tf#L40) -to ignore AMI changes. +GitLab team members can view more information in this internal handbook page on how to use FIPS AMI: +`https://internal.gitlab.com/handbook/engineering/fedramp-compliance/get-configure/#terraform---use-fips-ami` ##### Ansible: Specify the FIPS Omnibus builds @@ -167,17 +127,10 @@ The standard Omnibus GitLab releases build their own OpenSSL library, which is not FIPS-validated. However, we have nightly builds that create Omnibus packages that link against the operating system's OpenSSL library. To use this package, update the `gitlab_edition` and `gitlab_repo_script_url` fields in the Ansible -`vars.yml`. For example, you might modify -`gitlab-environment-toolkit/ansible/environments/gitlab-10k/inventory/vars.yml` -in this way: +`vars.yml`. -```yaml -all: - vars: - ... - gitlab_repo_script_url: "https://packages.gitlab.com/install/repositories/gitlab/gitlab-fips/script.deb.sh" - gitlab_edition: "gitlab-fips" -``` +GitLab team members can view more information in this internal handbook page on Ansible (AWS): +`https://internal.gitlab.com/handbook/engineering/fedramp-compliance/get-configure/#ansible-aws` #### Cloud Native Hybrid @@ -231,39 +184,16 @@ be different. Building a RHEL-based system with FIPS enabled should be possible, but there is [an outstanding issue preventing the Packer build from completing](https://github.com/aws-samples/amazon-eks-custom-amis/issues/51). -##### Terraform: Use a custom EKS AMI - -Now you can set the custom EKS AMI. - -1. In `environment.tf`, add `eks_ami_id = var.eks_ami_id` so you can pass this variable to the - AWS reference architecture module. For example, in - `gitlab-environment-toolkit/terraform/environments/gitlab-10k/inventory/environment.tf`: +Because this builds a custom AMI based on a specific version of an image, you must periodically rebuild the custom AMI to keep current with the latest security patches and upgrades. - ```tf - module "gitlab_ref_arch_aws" { - source = "../../modules/gitlab_ref_arch_aws" - - prefix = var.prefix - ami_id = data.aws_ami.ubuntu_20_04_fips[0].id - eks_ami_id = var.eks_ami_id - .... - ``` - -1. In `variables.tf`, define a `eks_ami_id` with the AMI ID in the - previous step: +##### Terraform: Use a custom EKS AMI - ```tf - variable "eks_ami_id" { - default = "ami-0a25e760cd00b027e" - } - ``` +GitLab team members can view more information in this internal handbook page on how to use a custom EKS AMI: +`https://internal.gitlab.com/handbook/engineering/fedramp-compliance/get-configure/#terraform---use-a-custom-eks-ami` ##### Ansible: Use UBI images -CNG uses a Helm Chart to manage which container images to deploy. By default, GET -deploys the latest released versions that use Debian-based containers. - -To switch to UBI-based containers, edit the Ansible `vars.yml` to use custom +CNG uses a Helm Chart to manage which container images to deploy. To use UBI-based containers, edit the Ansible `vars.yml` to use custom Charts variables: ```yaml diff --git a/doc/subscriptions/gitlab_dedicated/index.md b/doc/subscriptions/gitlab_dedicated/index.md index 08adcc7fa24..a2e04812876 100644 --- a/doc/subscriptions/gitlab_dedicated/index.md +++ b/doc/subscriptions/gitlab_dedicated/index.md @@ -124,7 +124,7 @@ The following GitLab application features are not available: - Service Desk - GitLab-managed runners (hosted runners) - GitLab AI capabilities (Refer to our [direction page](https://about.gitlab.com/direction/saas-platforms/dedicated/#supporting-ai-features-on-gitlab-dedicated) for more information) -- Any feature [not listed above](#available-features) which must be configured outside of the GitLab user interface. +- Features other than [available features](#available-features) that must be configured outside of the GitLab user interface including those behind feature flags The following features will not be supported: diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 00625ca0563..47f6e0ac1d9 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -11,9 +11,14 @@ This page contains information about the settings that are used on GitLab.com, a See some of these settings on the [instance configuration page](https://gitlab.com/help/instance_configuration) of GitLab.com. -## Unconfirmed user deletion +## Email confirmation -GitLab.com has the [`unconfirmed_users_delete_after_days`](../../administration/moderate_users.md#automatically-delete-unconfirmed-users) setting set to one day. +GitLab.com has the: + +- [`email_confirmation_setting`](../../administration/settings/sign_up_restrictions.md#confirm-user-email) + setting set to **Hard**. +- [`unconfirmed_users_delete_after_days`](../../administration/moderate_users.md#automatically-delete-unconfirmed-users) + setting set to one day. ## Password requirements diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 8550d64854c..7e6b9583a2a 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -687,6 +687,10 @@ For example, a reference like `#123+s` is rendered as URL references like `https://gitlab.com/gitlab-org/gitlab/-/issues/1234+s` are also expanded. +To update the rendered references if the assignee, milestone, or health status changed, +edit the comment or description and save it. +For more information, see issue [420807](https://gitlab.com/gitlab-org/gitlab/-/issues/420807). + ### Embedding metrics Metric charts can be embedded in GitLab Flavored Markdown. Read diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 70c88cad9d4..869e246f591 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21659,6 +21659,9 @@ msgstr "" msgid "GlobalSearch|No labels found" msgstr "" +msgid "GlobalSearch|Places" +msgstr "" + msgid "GlobalSearch|Project" msgstr "" diff --git a/spec/features/projects/settings/secure_files_spec.rb b/spec/features/projects/settings/secure_files_spec.rb index 7ff1a5f3568..5f94e215a5f 100644 --- a/spec/features/projects/settings/secure_files_spec.rb +++ b/spec/features/projects/settings/secure_files_spec.rb @@ -46,7 +46,7 @@ RSpec.describe 'Secure Files', :js, feature_category: :groups_and_projects do within '#js-secure-files' do expect(page).to have_content(file.name) - find('button.btn-danger-secondary').click + find('[data-testid="delete-button"]').click end expect(page).to have_content("Delete #{file.name}?") diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js index 4317f451377..dd8f39e7cb7 100644 --- a/spec/frontend/super_sidebar/components/context_switcher_spec.js +++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js @@ -15,7 +15,7 @@ import { trackContextAccess, formatContextSwitcherItems } from '~/super_sidebar/ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import waitForPromises from 'helpers/wait_for_promises'; import { stubComponent } from 'helpers/stub_component'; -import { searchUserProjectsAndGroupsResponseMock } from '../mock_data'; +import { contextSwitcherLinks, searchUserProjectsAndGroupsResponseMock } from '../mock_data'; jest.mock('~/super_sidebar/utils', () => ({ getStorageKeyFor: jest.requireActual('~/super_sidebar/utils').getStorageKeyFor, @@ -26,9 +26,6 @@ jest.mock('~/super_sidebar/utils', () => ({ })); const focusInputMock = jest.fn(); -const persistentLinks = [ - { title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' }, -]; const username = 'root'; const projectsPath = 'projectsPath'; const groupsPath = 'groupsPath'; @@ -71,8 +68,10 @@ describe('ContextSwitcher component', () => { wrapper = shallowMountExtended(ContextSwitcher, { apolloProvider: mockApollo, + provide: { + contextSwitcherLinks, + }, propsData: { - persistentLinks, username, projectsPath, groupsPath, @@ -107,14 +106,14 @@ describe('ContextSwitcher component', () => { createWrapper(); }); - it('renders the persistent links', () => { + it('renders the context switcher links', () => { const navItems = findNavItems(); const firstNavItem = navItems.at(0); - expect(navItems.length).toBe(persistentLinks.length); - expect(firstNavItem.props('item')).toBe(persistentLinks[0]); + expect(navItems.length).toBe(contextSwitcherLinks.length); + expect(firstNavItem.props('item')).toBe(contextSwitcherLinks[0]); expect(firstNavItem.props('linkClasses')).toEqual({ - [persistentLinks[0].link_classes]: persistentLinks[0].link_classes, + [contextSwitcherLinks[0].link_classes]: contextSwitcherLinks[0].link_classes, }); }); diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js index 52e9aa52c14..0fb6585e8ca 100644 --- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js @@ -4,36 +4,42 @@ import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue'; import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data'; +import { contextSwitcherLinks } from '../../../mock_data'; Vue.use(Vuex); describe('GlobalSearchDefaultItems', () => { let wrapper; - const createComponent = (initialState, props) => { + const createComponent = ({ + storeState, + mockDefaultSearchOptions = MOCK_DEFAULT_SEARCH_OPTIONS, + ...options + } = {}) => { const store = new Vuex.Store({ state: { searchContext: MOCK_SEARCH_CONTEXT, - ...initialState, + ...storeState, }, getters: { - defaultSearchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS, + defaultSearchOptions: () => mockDefaultSearchOptions, }, }); wrapper = shallowMountExtended(GlobalSearchDefaultItems, { store, - propsData: { - ...props, + provide: { + contextSwitcherLinks, }, stubs: { GlDisclosureDropdownGroup, }, + ...options, }); }; - const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem); - const findItemsData = () => findItems().wrappers.map((w) => w.props('item')); + const findGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup); + const findItems = (root = wrapper) => root.findAllComponents(GlDisclosureDropdownItem); describe('template', () => { describe('Dropdown items', () => { @@ -41,12 +47,39 @@ describe('GlobalSearchDefaultItems', () => { createComponent(); }); - it('renders item for each option in defaultSearchOptions', () => { - expect(findItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length); + it('renders two groups', () => { + const groups = findGroups(); + + expect(groups).toHaveLength(2); + + const actualNames = groups.wrappers.map((group) => group.props('group').name); + expect(actualNames).toEqual(['Places', 'All GitLab']); + }); + + it('renders context switcher links in first group', () => { + const group = findGroups().at(0); + expect(group.props('group').name).toBe('Places'); + + const items = findItems(group); + expect(items).toHaveLength(contextSwitcherLinks.length); + }); + + it('renders default search options in second group', () => { + const group = findGroups().at(1); + expect(group.props('group').name).toBe('All GitLab'); + + const items = findItems(group); + expect(items).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length); + }); + }); + + describe('Empty groups', () => { + beforeEach(() => { + createComponent({ mockDefaultSearchOptions: [], provide: { contextSwitcherLinks: [] } }); }); - it('provides the `item` prop to the `GlDisclosureDropdownItem` component', () => { - expect(findItemsData()).toStrictEqual(MOCK_DEFAULT_SEARCH_OPTIONS); + it('does not render groups with no items', () => { + expect(findGroups()).toHaveLength(0); }); }); @@ -55,13 +88,15 @@ describe('GlobalSearchDefaultItems', () => { ${null} | ${null} | ${'All GitLab'} ${{ name: 'Test Group' }} | ${null} | ${'Test Group'} ${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'} - `('Group Header', ({ group, project, groupHeader }) => { + `('Current context header', ({ group, project, groupHeader }) => { describe(`when group is ${group?.name} and project is ${project?.name}`, () => { beforeEach(() => { createComponent({ - searchContext: { - group, - project, + storeState: { + searchContext: { + group, + project, + }, }, }); }); diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js index ad7e7b0b30b..dfa8b458844 100644 --- a/spec/frontend/super_sidebar/components/global_search/mock_data.js +++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js @@ -62,6 +62,24 @@ export const MOCK_SEARCH_CONTEXT = { group_metadata: {}, }; +export const MOCK_GROUP_SEARCH_CONTEXT = { + ...MOCK_SEARCH_CONTEXT, + group: MOCK_GROUP, + group_metadata: { + issues_path: `${MOCK_GROUP.path}/issues`, + mr_path: `${MOCK_GROUP.path}/merge_requests`, + }, +}; + +export const MOCK_PROJECT_SEARCH_CONTEXT = { + ...MOCK_GROUP_SEARCH_CONTEXT, + project: MOCK_PROJECT, + project_metadata: { + issues_path: `${MOCK_PROJECT.path}/issues`, + mr_path: `${MOCK_PROJECT.path}/merge_requests`, + }, +}; + export const MOCK_DEFAULT_SEARCH_OPTIONS = [ { text: MSG_ISSUES_ASSIGNED_TO_ME, diff --git a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js index 68583d04b31..de636d1feec 100644 --- a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js +++ b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js @@ -7,6 +7,8 @@ import { MOCK_MR_PATH, MOCK_AUTOCOMPLETE_PATH, MOCK_SEARCH_CONTEXT, + MOCK_GROUP_SEARCH_CONTEXT, + MOCK_PROJECT_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_OPTIONS, MOCK_SCOPED_SEARCH_GROUP, @@ -74,37 +76,47 @@ describe('Global Search Store Getters', () => { }); describe.each` - group | group_metadata | project | project_metadata | expectedPath - ${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH} - ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'} - ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ issues_path: 'project/path' }} | ${'project/path'} - `('scopedIssuesPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => { - describe(`when group is ${group?.name} and project is ${project?.name}`, () => { - beforeEach(() => { - createState({ - searchContext: { - group, - group_metadata, - project, - project_metadata, - }, + group | group_metadata | project | project_metadata | user | expectedPath + ${null} | ${null} | ${null} | ${null} | ${'a_user'} | ${MOCK_ISSUE_PATH} + ${null} | ${null} | ${null} | ${null} | ${null} | ${false} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${null} | ${'group/path'} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ id: '123' }} | ${{ issues_path: 'project/path' }} | ${null} | ${'project/path'} + ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ id: '123' }} | ${{}} | ${null} | ${false} + `( + 'scopedIssuesPath', + ({ group, group_metadata, project, project_metadata, user, expectedPath }) => { + describe(`when group is ${group?.name} and project is ${project?.name}`, () => { + beforeEach(() => { + window.gon.current_username = user; + + createState({ + searchContext: { + group, + group_metadata, + project, + project_metadata, + }, + }); }); - }); - it(`should return ${expectedPath}`, () => { - expect(getters.scopedIssuesPath(state)).toBe(expectedPath); + it(`should return ${expectedPath}`, () => { + expect(getters.scopedIssuesPath(state)).toBe(expectedPath); + }); }); - }); - }); + }, + ); describe.each` - group | group_metadata | project | project_metadata | expectedPath - ${null} | ${null} | ${null} | ${null} | ${MOCK_MR_PATH} - ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${'group/path'} - ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${'project/path'} - `('scopedMRPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => { + group | group_metadata | project | project_metadata | user | expectedPath + ${null} | ${null} | ${null} | ${null} | ${'a_user'} | ${MOCK_MR_PATH} + ${null} | ${null} | ${null} | ${null} | ${null} | ${false} + ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${null} | ${'group/path'} + ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${null} | ${'project/path'} + `('scopedMRPath', ({ group, group_metadata, project, project_metadata, user, expectedPath }) => { describe(`when group is ${group?.name} and project is ${project?.name}`, () => { beforeEach(() => { + window.gon.current_username = user; + createState({ searchContext: { group, @@ -227,27 +239,88 @@ describe('Global Search Store Getters', () => { }); describe('defaultSearchOptions', () => { - const mockGetters = { - scopedIssuesPath: MOCK_ISSUE_PATH, - scopedMRPath: MOCK_MR_PATH, - }; + let mockGetters; beforeEach(() => { createState(); - window.gon.current_username = MOCK_USERNAME; + mockGetters = { + scopedIssuesPath: MOCK_ISSUE_PATH, + scopedMRPath: MOCK_MR_PATH, + }; }); - it('returns the correct array', () => { - expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( - MOCK_DEFAULT_SEARCH_OPTIONS, - ); + describe('with a user', () => { + beforeEach(() => { + window.gon.current_username = MOCK_USERNAME; + }); + + it('returns the correct array', () => { + expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_DEFAULT_SEARCH_OPTIONS, + ); + }); + + it('returns the correct array if issues path is false', () => { + mockGetters.scopedIssuesPath = undefined; + expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( + MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length), + ); + }); }); - it('returns the correct array if issues path is false', () => { - mockGetters.scopedIssuesPath = undefined; - expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual( - MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length), - ); + describe('without a user', () => { + describe('with no project or group context', () => { + beforeEach(() => { + mockGetters = { + scopedIssuesPath: false, + scopedMRPath: false, + }; + }); + + it('returns an empty array', () => { + expect(getters.defaultSearchOptions(state, mockGetters)).toEqual([]); + }); + }); + + describe('with a group context', () => { + beforeEach(() => { + createState({ + searchContext: MOCK_GROUP_SEARCH_CONTEXT, + }); + + mockGetters = { + scopedIssuesPath: state.searchContext.group_metadata.issues_path, + scopedMRPath: state.searchContext.group_metadata.mr_path, + }; + }); + + it('returns recent issues/merge requests options', () => { + expect(getters.defaultSearchOptions(state, mockGetters)).toEqual([ + { href: '/mock-group/issues', text: 'Recent issues' }, + { href: '/mock-group/merge_requests', text: 'Recent merge requests' }, + ]); + }); + }); + + describe('with a project context', () => { + beforeEach(() => { + createState({ + searchContext: MOCK_PROJECT_SEARCH_CONTEXT, + }); + + mockGetters = { + scopedIssuesPath: state.searchContext.project_metadata.issues_path, + scopedMRPath: state.searchContext.project_metadata.mr_path, + }; + }); + + it('returns recent issues/merge requests options', () => { + expect(getters.defaultSearchOptions(state, mockGetters)).toEqual([ + { href: '/mock-project/issues', text: 'Recent issues' }, + { href: '/mock-project/merge_requests', text: 'Recent merge requests' }, + ]); + }); + }); }); }); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index df45360a898..0d34329c60d 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -71,6 +71,10 @@ export const mergeRequestMenuGroup = [ }, ]; +export const contextSwitcherLinks = [ + { title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' }, +]; + export const sidebarData = { is_logged_in: true, current_menu_items: [], @@ -104,7 +108,7 @@ export const sidebarData = { gitlab_version_check: { severity: 'success' }, gitlab_com_and_canary: false, canary_toggle_com_url: 'https://next.gitlab.com', - context_switcher_links: [], + context_switcher_links: contextSwitcherLinks, search: { search_path: '/search', }, diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb index 69fb2bc43c0..81ab1b52552 100644 --- a/spec/graphql/types/ci/detailed_status_type_spec.rb +++ b/spec/graphql/types/ci/detailed_status_type_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Types::Ci::DetailedStatusType do include GraphqlHelpers - let_it_be(:stage) { create(:ci_stage, status: :manual) } + let_it_be(:stage) { create(:ci_stage, status: :skipped) } specify { expect(described_class.graphql_name).to eq('DetailedStatus') } diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index 3d675a65d99..4109eb01caa 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -374,11 +374,17 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do describe 'context switcher persistent links' do let_it_be(:public_link) do [ - { title: s_('Navigation|Your work'), link: '/', icon: 'work' }, { title: s_('Navigation|Explore'), link: '/explore', icon: 'compass' } ] end + let_it_be(:public_links_for_user) do + [ + { title: s_('Navigation|Your work'), link: '/', icon: 'work' }, + *public_link + ] + end + let_it_be(:admin_area_link) do { title: s_('Navigation|Admin Area'), link: '/admin', icon: 'admin' } end @@ -396,12 +402,20 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do helper.super_sidebar_context(user, group: nil, project: nil, panel: panel, panel_type: panel_type) end - context 'when user is not an admin' do - it 'returns only the public links' do + context 'when user is not logged in' do + let(:user) { nil } + + it 'returns only the public links for an anonymous user' do expect(subject[:context_switcher_links]).to eq(public_link) end end + context 'when user is not an admin' do + it 'returns only the public links for a user' do + expect(subject[:context_switcher_links]).to eq(public_links_for_user) + end + end + context 'when user is an admin' do before do allow(user).to receive(:admin?).and_return(true) @@ -420,7 +434,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do it 'returns public links, admin area and leave admin mode links' do expect(subject[:context_switcher_links]).to eq([ - *public_link, + *public_links_for_user, admin_area_link, leave_admin_mode_link ]) @@ -430,7 +444,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do context 'when admin mode is off' do it 'returns public links and enter admin mode link' do expect(subject[:context_switcher_links]).to eq([ - *public_link, + *public_links_for_user, enter_admin_mode_link ]) end @@ -444,7 +458,7 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do it 'returns public links and admin area link' do expect(subject[:context_switcher_links]).to eq([ - *public_link, + *public_links_for_user, admin_area_link ]) end diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb index 582c0fe1b1b..af8b5240e40 100644 --- a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt describe '#perform' do let(:job_artifact) { table(:ci_job_artifacts, database: :ci) } + let(:jobs) { table(:ci_builds, database: :ci) { |model| model.primary_key = :id } } let(:test_worker) do described_class.new( @@ -85,7 +86,7 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt private def create_job_artifact(id:, file_type:, expire_at:) - job = table(:ci_builds, database: :ci).create!(id: id, partition_id: 100) + job = jobs.create!(partition_id: 100) job_artifact.create!( id: id, job_id: job.id, expire_at: expire_at, project_id: project.id, file_type: file_type, partition_id: 100 diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb index 702341a7ea7..35d44281072 100644 --- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous_integration do +RSpec.describe Gitlab::Ci::Status::Stage::Factory do let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::Factory, feature_category: :continuous end context 'when stage has manual builds' do - Ci::HasStatus::BLOCKED_STATUS.each do |core_status| + (Ci::HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status| context "when status is #{core_status}" do let(:stage) { create(:ci_stage, pipeline: pipeline, status: core_status) } diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb index e23645c106b..9fdaddc083e 100644 --- a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb +++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Stage::PlayManual, feature_category: :continuous_integration do +RSpec.describe Gitlab::Ci::Status::Stage::PlayManual do let(:stage) { double('stage') } let(:play_manual) { described_class.new(stage) } @@ -48,7 +48,7 @@ RSpec.describe Gitlab::Ci::Status::Stage::PlayManual, feature_category: :continu context 'when stage is skipped' do let(:stage) { create(:ci_stage, status: :skipped) } - it { is_expected.to be_falsy } + it { is_expected.to be_truthy } end context 'when stage is manual' do diff --git a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb index d03d4f64a0f..9fa24e5637f 100644 --- a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb +++ b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_batch_spec.rb @@ -24,26 +24,6 @@ RSpec.describe Gitlab::Cleanup::OrphanJobArtifactFilesBatch do expect(batch.lost_and_found.count).to eq(1) expect(batch.lost_and_found.first.artifact_id).to eq(orphan_artifact.id) end - - it 'does not mix up job ID and artifact ID' do - # take maximum ID of both tables to avoid any collision - max_id = [Ci::Build.maximum(:id), Ci::JobArtifact.maximum(:id)].compact.max.to_i - job_a = create(:ci_build, id: max_id + 1) - job_b = create(:ci_build, id: max_id + 2) - # reuse the build IDs for the job artifact IDs, but swap them - job_artifact_b = create(:ci_job_artifact, :archive, job: job_b, id: max_id + 1) - job_artifact_a = create(:ci_job_artifact, :archive, job: job_a, id: max_id + 2) - - batch << artifact_path(job_artifact_a) - batch << artifact_path(job_artifact_b) - - job_artifact_b.delete - - batch.clean! - - expect(File.exist?(job_artifact_a.file.path)).to be_truthy - expect(File.exist?(job_artifact_b.file.path)).to be_falsey - end end context 'with dry run' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 641cc13adb7..6d01f480cd0 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -820,6 +820,7 @@ project: - scan_result_policy_reads - project_state - security_policy_bots +- target_branch_rules award_emoji: - awardable - user diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb index 575064f0bea..d69bf1a0da0 100644 --- a/spec/models/ci/runner_manager_spec.rb +++ b/spec/models/ci/runner_manager_spec.rb @@ -112,6 +112,16 @@ RSpec.describe Ci::RunnerManager, feature_category: :runner_fleet, type: :model end end + describe '.order_id_desc' do + subject(:scope) { described_class.order_id_desc } + + let_it_be(:runner_manager1) { create(:ci_runner_machine) } + let_it_be(:runner_manager2) { create(:ci_runner_machine) } + + specify { expect(described_class.all).to eq([runner_manager1, runner_manager2]) } + it { is_expected.to eq([runner_manager2, runner_manager1]) } + end + describe '#status', :freeze_time do let(:runner_manager) { build(:ci_runner_machine, created_at: 8.days.ago) } diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 1be50083cd4..79e92082ee1 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Stage, :models, feature_category: :continuous_integration do +RSpec.describe Ci::Stage, :models do let_it_be(:pipeline) { create(:ci_empty_pipeline) } let(:stage) { create(:ci_stage, pipeline: pipeline, project: pipeline.project) } diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index c8706ae9698..3f6d39435fd 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -34,67 +34,116 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do QUERY end - let(:query) do - %( - query { - runners(type:#{runner_type},status:#{status}) { - #{fields} + context 'with filters' do + let(:query) do + %( + query { + runners(type: #{runner_type}, status: #{status}) { + #{fields} + } } - } - ) - end - - before do - allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance| - allow(instance).to receive(:check_runner_upgrade_suggestion) + ) end - post_graphql(query, current_user: current_user) - end - - shared_examples 'a working graphql query returning expected runner' do - it_behaves_like 'a working graphql query' + before do + allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance| + allow(instance).to receive(:check_runner_upgrade_suggestion) + end - it 'returns expected runner' do - expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) + post_graphql(query, current_user: current_user) end - it 'does not execute more queries per runner', :aggregate_failures do - # warm-up license cache and so on: - personal_access_token = create(:personal_access_token, user: current_user) - args = { current_user: current_user, token: { personal_access_token: personal_access_token } } - post_graphql(query, **args) - expect(graphql_data_at(:runners, :nodes)).not_to be_empty + shared_examples 'a working graphql query returning expected runner' do + it_behaves_like 'a working graphql query' + + it 'returns expected runner' do + expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) + end + + it 'does not execute more queries per runner', :aggregate_failures do + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: current_user) + args = { current_user: current_user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + expect(graphql_data_at(:runners, :nodes)).not_to be_empty - admin2 = create(:admin) - personal_access_token = create(:personal_access_token, user: admin2) - args = { current_user: admin2, token: { personal_access_token: personal_access_token } } - control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } + admin2 = create(:admin) + personal_access_token = create(:personal_access_token, user: admin2) + args = { current_user: admin2, token: { personal_access_token: personal_access_token } } + control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } - create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2) - create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8], - creator: current_user) + create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2) + create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8], + creator: current_user) - expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) + expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) + end end - end - context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do - let(:runner_type) { 'INSTANCE_TYPE' } - let(:status) { 'ACTIVE' } + context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do + let(:runner_type) { 'INSTANCE_TYPE' } + let(:status) { 'ACTIVE' } - let!(:expected_runner) { instance_runner } + let!(:expected_runner) { instance_runner } - it_behaves_like 'a working graphql query returning expected runner' - end + it_behaves_like 'a working graphql query returning expected runner' + end - context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do - let(:runner_type) { 'PROJECT_TYPE' } - let(:status) { 'NEVER_CONTACTED' } + context 'runner_type is PROJECT_TYPE and status is NEVER_CONTACTED' do + let(:runner_type) { 'PROJECT_TYPE' } + let(:status) { 'NEVER_CONTACTED' } - let!(:expected_runner) { project_runner } + let!(:expected_runner) { project_runner } + + it_behaves_like 'a working graphql query returning expected runner' + end + end - it_behaves_like 'a working graphql query returning expected runner' + context 'without filters' do + context 'with managers requested for multiple runners' do + let(:fields) do + <<~QUERY + nodes { + managers { + nodes { + #{all_graphql_fields_for('CiRunnerManager', max_depth: 1)} + } + } + } + QUERY + end + + let(:query) do + %( + query { + runners { + #{fields} + } + } + ) + end + + it 'does not execute more queries per runner', :aggregate_failures do + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: current_user) + args = { current_user: current_user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + expect(graphql_data_at(:runners, :nodes)).not_to be_empty + + admin2 = create(:admin) + personal_access_token = create(:personal_access_token, user: admin2) + args = { current_user: admin2, token: { personal_access_token: personal_access_token } } + control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } + + create(:ci_runner, :instance, :with_runner_manager, version: '14.0.0', tag_list: %w[tag5 tag6], + creator: admin2) + create(:ci_runner, :project, :with_runner_manager, version: '14.0.1', projects: [project], + tag_list: %w[tag3 tag8], + creator: current_user) + + expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) + end + end end end diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb index fe8ee027245..5cb5724ebdc 100644 --- a/spec/serializers/stage_entity_spec.rb +++ b/spec/serializers/stage_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe StageEntity, feature_category: :continuous_integration do +RSpec.describe StageEntity do let(:pipeline) { create(:ci_pipeline) } let(:request) { double('request') } let(:user) { create(:user) } @@ -76,8 +76,8 @@ RSpec.describe StageEntity, feature_category: :continuous_integration do context 'with a skipped stage ' do let(:stage) { create(:ci_stage, status: 'skipped') } - it 'does not contain play_all_manual' do - expect(subject[:status][:action]).not_to be_present + it 'contains play_all_manual' do + expect(subject[:status][:action]).to be_present end end diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb index cdb1058e584..a2849e00d27 100644 --- a/spec/support/shared_examples/ci/stage_shared_examples.rb +++ b/spec/support/shared_examples/ci/stage_shared_examples.rb @@ -21,7 +21,7 @@ RSpec.shared_examples 'manual playable stage' do |stage_type| context 'when is skipped' do let(:status) { 'skipped' } - it { is_expected.to be_falsy } + it { is_expected.to be_truthy } end end end diff --git a/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb b/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb index 636b9870bd7..9da804b3140 100644 --- a/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb @@ -32,4 +32,6 @@ RSpec.shared_examples 'logged-out super-sidebar context' do it_behaves_like 'shared super sidebar context' it { is_expected.to include({ is_logged_in: false }) } + + it { expect(subject[:context_switcher_links]).to be_an(Array) } end diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb index 439a878a5e6..69e68f983fd 100644 --- a/spec/tooling/danger/stable_branch_spec.rb +++ b/spec/tooling/danger/stable_branch_spec.rb @@ -59,6 +59,8 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do end context 'when not applicable' do + let(:current_stable_branch) { '15-1-stable-ee' } + where(:stable_branch?, :security_mr?) do true | true false | true @@ -67,7 +69,7 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do with_them do before do - allow(fake_helper).to receive(:mr_target_branch).and_return(stable_branch? ? '15-1-stable-ee' : 'main') + allow(fake_helper).to receive(:mr_target_branch).and_return(stable_branch? ? current_stable_branch : 'main') allow(fake_helper).to receive(:security_mr?).and_return(security_mr?) end @@ -239,7 +241,7 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do end context 'when not an applicable version' do - let(:target_branch) { '14-9-stable-ee' } + let(:target_branch) { '15-0-stable-ee' } it 'warns about the package-and-test pipeline and the version' do expect(stable_branch).to receive(:warn).with(described_class::WARN_PACKAGE_AND_TEST_MESSAGE) @@ -297,18 +299,6 @@ RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do it_behaves_like 'without a failure' end - - context 'when too many version API requests are made' do - let(:parsed_response) { [{ 'version' => '15.0.0' }] } - - it 'adds a warning' do - expect(HTTParty).to receive(:get).and_return(version_response).at_least(10).times - expect(stable_branch).to receive(:warn).with(described_class::WARN_PACKAGE_AND_TEST_MESSAGE) - expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE) - - subject - end - end end end diff --git a/tooling/danger/stable_branch.rb b/tooling/danger/stable_branch.rb index efcd3cfc967..6335f82da37 100644 --- a/tooling/danger/stable_branch.rb +++ b/tooling/danger/stable_branch.rb @@ -146,26 +146,20 @@ module Tooling end def targeting_patchable_version? - raise VersionApiError if last_three_minor_versions.empty? + raise VersionApiError if current_stable_version.empty? - last_three_minor_versions.include?(targeted_version) + current_stable_version == targeted_version rescue VersionApiError warn FAILED_VERSION_REQUEST_MESSAGE true end - def last_three_minor_versions - return [] unless versions + def current_stable_version + return unless versions current_version = versions.first.match(VERSION_REGEX) - version_1 = previous_minor_version(current_version) - version_2 = previous_minor_version(version_1) - [ - version_to_minor_string(current_version), - version_to_minor_string(version_1), - version_to_minor_string(version_2) - ] + version_to_minor_string(current_version) end def targeted_version @@ -173,7 +167,7 @@ module Tooling end def versions(page = 1) - version_api_endpoint = "https://version.gitlab.com/api/v1/versions?per_page=50&page=#{page}" + version_api_endpoint = "https://version.gitlab.com/api/v1/versions?per_page=20&page=#{page}" response = HTTParty.get(version_api_endpoint) # rubocop:disable Gitlab/HTTParty raise VersionApiError unless response.success? @@ -183,33 +177,6 @@ module Tooling version_list.sort_by { |v| Gem::Version.new(v) }.reverse end - def previous_minor_version(version) - previous_minor = version[:minor].to_i - 1 - - return "#{version[:major]}.#{previous_minor}".match(VERSION_REGEX) if previous_minor >= 0 - - fetch_last_minor_version_for_major(version[:major].to_i - 1) - end - - def fetch_last_minor_version_for_major(major) - page = 1 - last_minor_version = nil - - while last_minor_version.nil? - last_minor_version = versions(page).find do |version| - version.split('.').first.to_i == major - end - - break if page > 10 - - page += 1 - end - - raise VersionApiError if last_minor_version.nil? - - last_minor_version.match(VERSION_REGEX) - end - def version_to_minor_string(version) "#{version[:major]}.#{version[:minor]}" end |