diff --git a/Gemfile b/Gemfile index 074777c173d..13806f67a13 100644 --- a/Gemfile +++ b/Gemfile @@ -588,7 +588,7 @@ group :test do gem 'graphlyte', '~> 1.0.0', feature_category: :shared - gem 'shoulda-matchers', '~> 5.1.0', require: false, feature_category: :shared + gem 'shoulda-matchers', '~> 6.4.0', require: false, feature_category: :shared gem 'email_spec', '~> 2.3.0', feature_category: :shared gem 'webmock', '~> 3.25.0', feature_category: :shared gem 'rails-controller-testing', feature_category: :shared diff --git a/Gemfile.checksum b/Gemfile.checksum index e0454c4ffd6..e8b945d2e18 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -685,7 +685,7 @@ {"name":"sentry-ruby","version":"5.22.1","platform":"ruby","checksum":"ed77bdd76da7a4c6a3de43dc6d19d3c0412b2675b014a2654bc5bafd4d5b3289"}, {"name":"sentry-sidekiq","version":"5.22.1","platform":"ruby","checksum":"bd7a3f915e58e13ea67251d9a458667fc4bee6dfbbd12614c47daa239e822a89"}, {"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"}, -{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"}, +{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"}, {"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"}, {"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"}, {"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"}, diff --git a/Gemfile.lock b/Gemfile.lock index dd5b63dfd4b..76b1484d31c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1751,7 +1751,7 @@ GEM sentry-ruby (~> 5.22.1) sidekiq (>= 3.0) shellany (0.0.1) - shoulda-matchers (5.1.0) + shoulda-matchers (6.4.0) activesupport (>= 5.2.0) sidekiq-cron (1.12.0) fugit (~> 1.8) @@ -2312,7 +2312,7 @@ DEPENDENCIES sentry-rails (~> 5.22.0) sentry-ruby (~> 5.22.0) sentry-sidekiq (~> 5.22.0) - shoulda-matchers (~> 5.1.0) + shoulda-matchers (~> 6.4.0) sidekiq! sidekiq-cron (~> 1.12.0) sigdump (~> 0.2.4) diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum index d56fc82b8c8..62f6d5323be 100644 --- a/Gemfile.next.checksum +++ b/Gemfile.next.checksum @@ -696,7 +696,7 @@ {"name":"sentry-ruby","version":"5.22.1","platform":"ruby","checksum":"ed77bdd76da7a4c6a3de43dc6d19d3c0412b2675b014a2654bc5bafd4d5b3289"}, {"name":"sentry-sidekiq","version":"5.22.1","platform":"ruby","checksum":"bd7a3f915e58e13ea67251d9a458667fc4bee6dfbbd12614c47daa239e822a89"}, {"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"}, -{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"}, +{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"}, {"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"}, {"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"}, {"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"}, diff --git a/Gemfile.next.lock b/Gemfile.next.lock index be7eb183cce..215b26e2a6c 100644 --- a/Gemfile.next.lock +++ b/Gemfile.next.lock @@ -1784,7 +1784,7 @@ GEM sentry-ruby (~> 5.22.1) sidekiq (>= 3.0) shellany (0.0.1) - shoulda-matchers (5.1.0) + shoulda-matchers (6.4.0) activesupport (>= 5.2.0) sidekiq-cron (1.12.0) fugit (~> 1.8) @@ -2346,7 +2346,7 @@ DEPENDENCIES sentry-rails (~> 5.22.0) sentry-ruby (~> 5.22.0) sentry-sidekiq (~> 5.22.0) - shoulda-matchers (~> 5.1.0) + shoulda-matchers (~> 6.4.0) sidekiq! sidekiq-cron (~> 1.12.0) sigdump (~> 0.2.4) diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue index 758f61e4970..f07ed831f58 100644 --- a/app/assets/javascripts/ci/job_details/job_app.vue +++ b/app/assets/javascripts/ci/job_details/job_app.vue @@ -365,6 +365,7 @@ export default { :class="{ 'right-sidebar-expanded': isSidebarOpen, 'right-sidebar-collapsed': !isSidebarOpen, + '!gl-bottom-8': displayStickyFooter, }" :artifact-help-url="artifactHelpUrl" data-testid="job-sidebar" diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js index 40f92763b29..4d7a9794f5d 100644 --- a/app/assets/javascripts/issuable/index.js +++ b/app/assets/javascripts/issuable/index.js @@ -1,10 +1,8 @@ -import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import Sidebar from '~/right_sidebar'; import { getSidebarOptions } from '~/sidebar/mount_sidebar'; import CsvImportExportButtons from './components/csv_import_export_buttons.vue'; -import IssuableByEmail from './components/issuable_by_email.vue'; import issuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableContext from './issuable_context'; @@ -63,39 +61,6 @@ export function initCsvImportExportButtons() { }); } -export function initIssuableByEmail() { - const el = document.querySelector('.js-issuable-by-email'); - - if (!el) { - return null; - } - - Vue.use(GlToast); - - const { - initialEmail, - issuableType, - emailsHelpPagePath, - quickActionsHelpPath, - markdownHelpPath, - resetPath, - } = el.dataset; - - return new Vue({ - el, - name: 'IssuableByEmailRoot', - provide: { - initialEmail, - issuableType, - emailsHelpPagePath, - quickActionsHelpPath, - markdownHelpPath, - resetPath, - }, - render: (createElement) => createElement(IssuableByEmail), - }); -} - export function initIssuableSidebar() { const el = document.querySelector('.js-sidebar-options'); diff --git a/app/assets/javascripts/merge_requests/list/index.js b/app/assets/javascripts/merge_requests/list/index.js index 613933bd8a0..1cdf2c3ce86 100644 --- a/app/assets/javascripts/merge_requests/list/index.js +++ b/app/assets/javascripts/merge_requests/list/index.js @@ -4,7 +4,6 @@ import VueRouter from 'vue-router'; import { parseBoolean } from '~/lib/utils/common_utils'; import { defaultClient } from '~/graphql_shared/issuable_client'; import MergeRequestsListApp from './components/merge_requests_list_app.vue'; -import MoreactionsDropdown from './components/more_actions_dropdown.vue'; export async function mountMergeRequestListsApp({ getMergeRequestsQuery, @@ -94,36 +93,3 @@ export async function mountMergeRequestListsApp({ render: (createComponent) => createComponent(MergeRequestsListApp), }); } - -export async function mountMoreActionsDropdown() { - const el = document.querySelector('#js-vue-mr-list-more-actions'); - - if (!el) { - return null; - } - - const { - isSignedIn, - showExportButton, - issuableType, - issuableCount, - email, - exportCsvPath, - rssUrl, - } = el.dataset; - - return new Vue({ - el, - name: 'MergeRequestsListMoreActions', - provide: { - isSignedIn: parseBoolean(isSignedIn), - showExportButton: parseBoolean(showExportButton), - issuableType, - issuableCount: Number(issuableCount), - email, - exportCsvPath, - rssUrl, - }, - render: (createComponent) => createComponent(MoreactionsDropdown), - }); -} diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue index 7234806b3bb..1d7761cf670 100644 --- a/app/assets/javascripts/milestones/components/milestone_combobox.vue +++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue @@ -4,13 +4,11 @@ import { debounce, isEqual } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; import { s__, __, sprintf } from '~/locale'; -import createStore from '../stores'; const SEARCH_DEBOUNCE_MS = 250; export default { name: 'MilestoneCombobox', - store: createStore(), components: { GlCollapsibleListbox, GlBadge, @@ -50,8 +48,8 @@ export default { unselect: __('Unselect'), }, computed: { - ...mapState(['matches', 'selectedMilestones']), - ...mapGetters(['isLoading']), + ...mapState('milestoneCombobox', ['matches', 'selectedMilestones']), + ...mapGetters('milestoneCombobox', ['isLoading']), allMilestones() { const { groupMilestones, projectMilestones } = this.matches || {}; const milestones = []; @@ -125,7 +123,7 @@ export default { this.fetchMilestones(); }, methods: { - ...mapActions([ + ...mapActions('milestoneCombobox', [ 'setProjectId', 'setGroupId', 'setGroupMilestonesAvailable', diff --git a/app/assets/javascripts/milestones/stores/index.js b/app/assets/javascripts/milestones/stores/index.js index 44ad5468dcd..934cb7f316c 100644 --- a/app/assets/javascripts/milestones/stores/index.js +++ b/app/assets/javascripts/milestones/stores/index.js @@ -8,10 +8,19 @@ import createState from './state'; Vue.use(Vuex); +export const createMilestoneComboboxModule = () => ({ + actions, + getters, + mutations, + state: createState(), +}); + export default () => new Vuex.Store({ - actions, - getters, - mutations, - state: createState(), + modules: { + milestoneCombobox: { + namespaced: true, + ...createMilestoneComboboxModule(), + }, + }, }); diff --git a/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue b/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue index e55fa2b65d6..23d8972b83f 100644 --- a/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue +++ b/app/assets/javascripts/ml/experiment_tracking/components/model_experiments_header.vue @@ -40,12 +40,16 @@ export default { }; }, modelsCountLabel() { - return n__('MlModelRegistry|%d experiment', 'MlModelRegistry|%d experiments', this.count); + return n__( + 'MlExperimentTracking|%d experiment', + 'MlExperimentTracking|%d experiments', + this.count, + ); }, }, i18n: { - createTitle: s__('MlModelRegistry|Create'), - importMlflow: s__('MlModelRegistry|Create experiments using MLflow'), + createTitle: s__('MlExperimentTracking|Create'), + importMlflow: s__('MlExperimentTracking|Create experiments using MLflow'), }, mlflowModalId: MLFLOW_USAGE_MODAL_ID, }; diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/model_selection_dropdown.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/model_selection_dropdown.vue index 07389737e3d..88ebd84a7a8 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/model_selection_dropdown.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/model_selection_dropdown.vue @@ -77,8 +77,8 @@ export default { }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), }, i18n: { - noResultsMessage: s__('MlModelRegistry|No results'), - emptyFieldPlaceholder: s__('MlModelRegistry|Select a model'), + noResultsMessage: s__('MlExperimentTracking|No results'), + emptyFieldPlaceholder: s__('MlExperimentTracking|Select a model'), }, }; diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/promote_run.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/promote_run.vue index c3f686c57f6..6eea1e59eb1 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/promote_run.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/promote/promote_run.vue @@ -43,13 +43,15 @@ export default { versionDescription() { if (this.latestVersion) { return sprintf( - s__('MlModelRegistry|Must be a semantic version. Latest version is %{latestVersion}'), + s__( + 'MlExperimentTracking|Must be a semantic version. Latest version is %{latestVersion}', + ), { latestVersion: this.latestVersion, }, ); } - return s__('MlModelRegistry|Must be a semantic version.'); + return s__('MlExperimentTracking|Must be a semantic version.'); }, autocompleteDataSources() { return gl.GfmAutoComplete?.dataSources; @@ -123,26 +125,28 @@ export default { }, }, descriptionFormFieldProps: { - placeholder: s__('MlModelRegistry|Enter a model version description'), + placeholder: s__('MlExperimentTracking|Enter a model version description'), id: 'model-version-description', name: 'model-version-description', }, i18n: { - actionPrimaryText: s__('MlModelRegistry|Promote'), + actionPrimaryText: s__('MlExperimentTracking|Promote'), actionSecondaryText: __('Cancel'), - versionDescription: s__('MlModelRegistry|Enter a semantic version.'), - versionValid: s__('MlModelRegistry|Version is valid semantic version.'), - versionInvalid: s__('MlModelRegistry|Version is not a valid semantic version.'), - versionPlaceholder: s__('MlModelRegistry|For example 1.0.0'), - descriptionPlaceholder: s__('MlModelRegistry|Enter some description'), - title: s__('MlModelRegistry|Promote run'), - description: s__('MlModelRegistry|Complete the form below to promote run to a model version.'), - optionalText: s__('MlModelRegistry|(Optional)'), - versionLabelText: s__('MlModelRegistry|Version'), - versionDescriptionText: s__('MlModelRegistry|Description'), - modelSelectionLabelText: s__('MlModelRegistry|Model'), + versionDescription: s__('MlExperimentTracking|Enter a semantic version.'), + versionValid: s__('MlExperimentTracking|Version is valid semantic version.'), + versionInvalid: s__('MlExperimentTracking|Version is not a valid semantic version.'), + versionPlaceholder: s__('MlExperimentTracking|For example 1.0.0'), + descriptionPlaceholder: s__('MlExperimentTracking|Enter some description'), + title: s__('MlExperimentTracking|Promote run'), + description: s__( + 'MlExperimentTracking|Complete the form below to promote run to a model version.', + ), + optionalText: s__('MlExperimentTracking|(Optional)'), + versionLabelText: s__('MlExperimentTracking|Version'), + versionDescriptionText: s__('MlExperimentTracking|Description'), + modelSelectionLabelText: s__('MlExperimentTracking|Model'), modelDescription: s__( - 'MlModelRegistry|Select the model that will contain the new version. The run will move to the default experiment of that model.', + 'MlExperimentTracking|Select the model that will contain the new version. The run will move to the default experiment of that model.', ), }, }; diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/candidate_detail.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/candidate_detail.vue index 51a0c99eaed..2deb7728273 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/candidate_detail.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/candidate_detail.vue @@ -55,11 +55,11 @@ export default { const fields = range(maxStep + 1).map((step) => ({ key: step.toString(), - label: sprintf(s__('MlModelRegistry|Step %{step}'), { step }), + label: sprintf(s__('MlExperimentTracking|Step %{step}'), { step }), ...cssClasses, })); - return [{ key: 'name', label: s__('MlModelRegistry|Metric'), ...cssClasses }, ...fields]; + return [{ key: 'name', label: s__('MlExperimentTracking|Metric'), ...cssClasses }, ...fields]; }, metricsTableItems() { const items = {}; @@ -87,20 +87,20 @@ export default { }, }, i18n: { - detailsLabel: s__('MlModelRegistry|Details & Metadata'), - artifactsLabel: s__('MlModelRegistry|Artifacts'), - mlflowIdLabel: s__('MlModelRegistry|MLflow run ID'), - ciSectionLabel: s__('MlModelRegistry|CI Info'), + detailsLabel: s__('MlExperimentTracking|Details & Metadata'), + artifactsLabel: s__('MlExperimentTracking|Artifacts'), + mlflowIdLabel: s__('MlExperimentTracking|MLflow run ID'), + ciSectionLabel: s__('MlExperimentTracking|CI Info'), jobLabel: __('Job'), - ciUserLabel: s__('MlModelRegistry|Triggered by'), + ciUserLabel: s__('MlExperimentTracking|Triggered by'), ciMrLabel: __('Merge request'), - parametersLabel: s__('MlModelRegistry|Parameters'), - performanceLabel: s__('MlModelRegistry|Performance'), - noParametersMessage: s__('MlModelRegistry|No logged parameters'), - noMetricsMessage: s__('MlModelRegistry|No logged metrics'), - noMetadataMessage: s__('MlModelRegistry|No logged metadata'), - noCiMessage: s__('MlModelRegistry|Run not linked to a CI build'), - noArtifactsMessage: s__('MlModelRegistry|No logged artifacts.'), + parametersLabel: s__('MlExperimentTracking|Parameters'), + performanceLabel: s__('MlExperimentTracking|Performance'), + noParametersMessage: s__('MlExperimentTracking|No logged parameters'), + noMetricsMessage: s__('MlExperimentTracking|No logged metrics'), + noMetadataMessage: s__('MlExperimentTracking|No logged metadata'), + noCiMessage: s__('MlExperimentTracking|Run not linked to a CI build'), + noArtifactsMessage: s__('MlExperimentTracking|No logged artifacts.'), copyMessage: __('Copy MLflow run ID'), }, }; diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue index 4cf40158670..e300343c50b 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index.vue @@ -68,7 +68,7 @@ export default { }, error(error) { this.errorMessage = sprintf( - s__('MlModelRegistry|Failed to load experiments with error: %{error}'), + s__('MlExperimentTracking|Failed to load experiments with error: %{error}'), { error: error.message }, ); Sentry.captureException(error); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 8afd84f8df4..aa836d8ac1f 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,32 +1,13 @@ -import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import getMergeRequestsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests.query.graphql'; import getMergeRequestsCountsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests_counts.query.graphql'; import getMergeRequestsApprovalsQuery from 'ee_else_ce/merge_requests/list/queries/group/get_merge_requests_approvals.query.graphql'; -import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; -import { FILTERED_SEARCH } from '~/filtered_search/constants'; import { initBulkUpdateSidebar } from '~/issuable'; -import initFilteredSearch from '~/pages/search/init_filtered_search'; -import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; -import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants'; -import searchUserGroupProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql'; import { mountMergeRequestListsApp } from '~/merge_requests/list'; const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_'; -addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX); -initFilteredSearch({ - page: FILTERED_SEARCH.MERGE_REQUESTS, - isGroupDecendent: true, - useDefaultState: true, - filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, -}); -initNewResourceDropdown({ - resourceType: RESOURCE_TYPE_MERGE_REQUEST, - query: searchUserGroupProjectsWithMergeRequestsEnabled, - extractProjects: (data) => data?.group?.projects?.nodes, -}); mountMergeRequestListsApp({ getMergeRequestsQuery, getMergeRequestsCountsQuery, diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index a04d64a488a..cd6a91d985e 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -1,31 +1,15 @@ -import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests'; import getMergeRequestsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests.query.graphql'; import getMergeRequestsCountsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests_counts.query.graphql'; import getMergeRequestsApprovalsQuery from 'ee_else_ce/merge_requests/list/queries/project/get_merge_requests_approvals.query.graphql'; import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; -import { FILTERED_SEARCH } from '~/filtered_search/constants'; -import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable'; -import initFilteredSearch from '~/pages/search/init_filtered_search'; -import { mountMoreActionsDropdown, mountMergeRequestListsApp } from '~/merge_requests/list'; +import { initBulkUpdateSidebar } from '~/issuable'; +import { mountMergeRequestListsApp } from '~/merge_requests/list'; initBulkUpdateSidebar('merge_request_'); -addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); -IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration'); - -initFilteredSearch({ - page: FILTERED_SEARCH.MERGE_REQUESTS, - filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, - useDefaultState: true, -}); - addShortcutsExtension(ShortcutsNavigation); -initIssuableByEmail(); -initCsvImportExportButtons(); -mountMoreActionsDropdown(); mountMergeRequestListsApp({ getMergeRequestsQuery, getMergeRequestsApprovalsQuery, diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue index 2c09558f87d..5848ef3ce6c 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue @@ -33,7 +33,7 @@ export default { const bars = [ { name: s__('Pipeline|Successful'), data: [] }, { name: s__('Pipeline|Failed'), data: [] }, - { name: s__('Pipeline|Other'), data: [] }, + { name: s__('Pipeline|Other (Cancelled, Skipped)'), data: [] }, ]; this.timeSeries.forEach(({ label, successCount, failedCount, otherCount }) => { diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js index ae67d5eba35..d35e924c71d 100644 --- a/app/assets/javascripts/releases/mount_edit.js +++ b/app/assets/javascripts/releases/mount_edit.js @@ -1,6 +1,7 @@ import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; +import { createMilestoneComboboxModule } from '~/milestones/stores'; import ReleaseEditNewApp from './components/app_edit_new.vue'; import createStore from './stores'; import createEditNewModule from './stores/modules/edit_new'; @@ -12,6 +13,7 @@ export default () => { const store = createStore({ modules: { + milestoneCombobox: { namespaced: true, ...createMilestoneComboboxModule() }, editNew: createEditNewModule({ ...el.dataset, isExistingRelease: true }), }, }); diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js index ff8da047061..7b006d35953 100644 --- a/app/assets/javascripts/releases/mount_new.js +++ b/app/assets/javascripts/releases/mount_new.js @@ -1,6 +1,7 @@ import Vue from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; +import { createMilestoneComboboxModule } from '~/milestones/stores'; import { createRefModule } from '../ref/stores'; import ReleaseEditNewApp from './components/app_edit_new.vue'; import createStore from './stores'; @@ -13,6 +14,7 @@ export default () => { const store = createStore({ modules: { + milestoneCombobox: { namespaced: true, ...createMilestoneComboboxModule() }, editNew: createEditNewModule({ ...el.dataset, isExistingRelease: false }), ref: createRefModule(), }, diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 63ef426c60d..ab5c136fee3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -149,11 +149,7 @@ class GroupsController < Groups::ApplicationController @badge_api_endpoint = expose_path(api_v4_groups_badges_path(id: @group.id)) end - def merge_requests - return if ::Feature.enabled?(:vue_merge_request_list, current_user) - - render_merge_requests - end + def merge_requests; end def projects @projects = @group.projects.with_statistics.page(params[:page]) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 244267bfadd..0e450f56bb7 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -390,7 +390,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private def set_issuables_index - return if ::Feature.enabled?(:vue_merge_request_list, current_user) && request.format.html? + return if request.format.html? super end diff --git a/app/controllers/user_settings/active_sessions_controller.rb b/app/controllers/user_settings/active_sessions_controller.rb index 34359eb0080..f67c1d718f9 100644 --- a/app/controllers/user_settings/active_sessions_controller.rb +++ b/app/controllers/user_settings/active_sessions_controller.rb @@ -11,7 +11,7 @@ module UserSettings def destroy # params[:id] can be an Rack::Session::SessionId#private_id ActiveSession.destroy_session(current_user, params[:id]) - current_user.forget_me! + current_user.invalidate_all_remember_tokens! respond_to do |format| format.html { redirect_to user_settings_active_sessions_url, status: :found } diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb index 0913bc875ec..fe05c69470a 100644 --- a/app/models/packages/protection/rule.rb +++ b/app/models/packages/protection/rule.rb @@ -42,6 +42,11 @@ module Packages scope :for_package_type, ->(package_type) { where(package_type: package_type) } + def self.for_delete_exists?(access_level:, package_name:, package_type:) + for_action_exists?(action: :delete, access_level: access_level, package_name: package_name, + package_type: package_type) + end + def self.for_action_exists?(action:, access_level:, package_name:, package_type:) return false if [access_level, package_name, package_type].any?(&:blank?) diff --git a/app/models/user.rb b/app/models/user.rb index 98936f41b62..b0f7d19567f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1215,6 +1215,19 @@ class User < ApplicationRecord super if ::Gitlab::Database.read_write? end + # This is a copy of #forget_me! without the check for `expire_all_remember_me_on_sign_out` + # https://github.com/heartcombo/devise/blob/v4.9.4/lib/devise/models/rememberable.rb#L58-L63 + # + # We need a separate method because we disabled that setting but we also need to be able to + # manually expire these tokens when a session is manually destroyed + def invalidate_all_remember_tokens! + return unless persisted? + + self.remember_token = nil if respond_to?(:remember_token) + self.remember_created_at = nil + save(validate: false) + end + # Override Devise Rememberable#remember_me? # # In Devise this method compares the remember me token received from the user session diff --git a/app/services/packages/protection/check_delete_rule_existence_service.rb b/app/services/packages/protection/check_delete_rule_existence_service.rb new file mode 100644 index 00000000000..c7d162c714b --- /dev/null +++ b/app/services/packages/protection/check_delete_rule_existence_service.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Packages + module Protection + class CheckDeleteRuleExistenceService < BaseProjectService + SUCCESS_RESPONSE_RULE_EXISTS = ServiceResponse.success(payload: { protection_rule_exists?: true }).freeze + SUCCESS_RESPONSE_RULE_DOESNT_EXIST = ServiceResponse.success(payload: { protection_rule_exists?: false }).freeze + + ERROR_RESPONSE_UNAUTHORIZED = ServiceResponse.error(message: 'Unauthorized', reason: :unauthorized).freeze + ERROR_RESPONSE_INVALID_PACKAGE_TYPE = ServiceResponse.error(message: 'Invalid package type', + reason: :invalid_package_type).freeze + + def execute + return ERROR_RESPONSE_INVALID_PACKAGE_TYPE unless package_type_allowed? + return ERROR_RESPONSE_UNAUTHORIZED unless current_user_can_destroy_package? + return SUCCESS_RESPONSE_RULE_DOESNT_EXIST if current_user.can_admin_all_resources? + + user_project_authorization_access_level = current_user.max_member_access_for_project(project.id) + + response = project.package_protection_rules.for_delete_exists?( + access_level: user_project_authorization_access_level, + package_name: params[:package_name], + package_type: params[:package_type] + ) + + service_response_for(response) + end + + private + + def package_type_allowed? + Packages::Protection::Rule.package_types.key?(params[:package_type]) + end + + def current_user_can_destroy_package? + can?(current_user, :destroy_package, project) + end + + def service_response_for(protection_rule_exists) + protection_rule_exists ? SUCCESS_RESPONSE_RULE_EXISTS : SUCCESS_RESPONSE_RULE_DOESNT_EXIST + end + end + end +end diff --git a/app/services/packages/protection/check_rule_existence_service.rb b/app/services/packages/protection/check_rule_existence_service.rb index 8a705e3bda0..d97ae998384 100644 --- a/app/services/packages/protection/check_rule_existence_service.rb +++ b/app/services/packages/protection/check_rule_existence_service.rb @@ -34,8 +34,7 @@ module Packages return false if current_user.can_admin_all_resources? user_project_authorization_access_level = current_user.max_member_access_for_project(project.id) - project.package_protection_rules - .for_action_exists?( + project.package_protection_rules.for_action_exists?( action: :push, access_level: user_project_authorization_access_level, package_name: params[:package_name], diff --git a/app/validators/json_schemas/user_detail_onboarding_status.json b/app/validators/json_schemas/user_detail_onboarding_status.json index 5d2f9bcdf75..7fac1442c4d 100644 --- a/app/validators/json_schemas/user_detail_onboarding_status.json +++ b/app/validators/json_schemas/user_detail_onboarding_status.json @@ -61,7 +61,8 @@ 5, 6, 7, - 8 + 8, + 99 ] } }, diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 56a2075d2dc..ca4be4a8bd9 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -2,27 +2,6 @@ - page_title _("Merge requests") - add_page_specific_style 'page_bundles/issuable_list' -- if Feature.enabled?(:vue_merge_request_list, @group) - .js-merge-request-list-root{ data: group_merge_requests_list_data(@group, current_user) } - - if has_bulk_update_permission - = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests -- else - - can_bulk_update = has_bulk_update_permission && issuables_count_for_state(:merge_requests, :all) > 0 - - .top-area - = render 'shared/issuable/nav', type: :merge_requests, display_count: !@search_timeout_occurred - - if current_user - .nav-controls - - if can_bulk_update - = render_if_exists 'projects/merge_requests/bulk_update_button' - - = render 'shared/new_project_item_vue_select' - - = render 'shared/issuable/search_bar', type: :merge_requests - - if can_bulk_update - = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests - - - if @search_timeout_occurred - = render 'shared/dashboard/search_timeout_occurred' - - else - = render 'shared/merge_requests' +.js-merge-request-list-root{ data: group_merge_requests_list_data(@group, current_user) } +- if has_bulk_update_permission + = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml deleted file mode 100644 index 130d0c848a2..00000000000 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -= render_if_exists 'projects/merge_requests/merge_trains_button' -- if @can_bulk_update - = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do - = _("Bulk edit") -- if merge_project && can?(@current_user, :create_merge_request_in, @project) - = render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm, - button_options: { data: { event_tracking: 'click_new_merge_request_list' } }) do - = _("New merge request") - -#js-vue-mr-list-more-actions{ data: project_merge_requests_list_more_actions_data(@project, current_user) } diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index b626620d70e..9d571a7b0b2 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,10 +1,6 @@ - @can_bulk_update = can?(current_user, :admin_merge_request, @project) -- merge_project = merge_request_source_project_for_project(@project) -- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project -- issuable_type = 'merge_request' - page_title _("Merge requests") -- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request') - add_page_specific_style 'page_bundles/issuable_list' - add_page_specific_style 'page_bundles/merge_request' @@ -13,27 +9,6 @@ = render 'projects/last_push' -- if Feature.enabled?(:vue_merge_request_list, @project) - .js-merge-request-list-root{ data: project_merge_requests_list_data(@project, current_user) } - - if @can_bulk_update - = render 'shared/issuable/bulk_update_sidebar', type: :merge_requests -- else - - if @project.merge_requests.exists? - .top-area - = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path - - = render 'shared/issuable/search_bar', type: :merge_requests - - - if @can_bulk_update - = render 'shared/issuable/bulk_update_sidebar', type: :merge_requests - - .merge-requests-holder - = render 'merge_requests', new_merge_request_path: new_merge_request_path - - else - = render 'shared/empty_states/merge_requests', button_path: new_merge_request_path - - - if new_merge_request_email && can?(current_user, :create_merge_request_in, @project) - .gl-text-center.gl-pt-5.gl-pb-7 - .js-issuable-by-email{ data: { initial_email: new_merge_request_email, issuable_type: issuable_type, emails_help_page_path: help_page_path('development/emails.md', anchor: 'email-namespace'), quick_actions_help_path: help_page_path('user/project/quick_actions.md'), markdown_help_path: help_page_path('user/markdown.md'), reset_path: new_issuable_address_project_path(@project, issuable_type: issuable_type) } } +.js-merge-request-list-root{ data: project_merge_requests_list_data(@project, current_user) } +- if @can_bulk_update + = render 'shared/issuable/bulk_update_sidebar', type: :merge_requests diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index ddc6c6cde96..ac051cc84a5 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -11,11 +11,10 @@ = _('Update selected') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-bulk-update-menu-hide gl-float-right' }) do = _('Cancel') - - if params[:state] != 'merged' || ::Feature.enabled?(:vue_merge_request_list, @project) - .block.js-status-dropdown-container - .title - = _('Status') - .js-status-dropdown + .block.js-status-dropdown-container + .title + = _('Status') + .js-status-dropdown .block .title = _('Assignee') diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 7e4a2ee26dc..e0a90894a7c 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -101,19 +101,19 @@ = render_if_exists 'shared/projects/badges', project: project - if stars - = link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do + = link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} star', '%{project} has %{number} stars', project.star_count), number: project.star_count, project: project.name) } do = sprite_icon('star-o', size: 14, css_class: 'gl-mr-2') = badge_count(project.star_count) - if show_count?(disabled: !forks, compact_mode: compact_mode) - = link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do + = link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} fork', '%{project} has %{number} forks', project.forks_count), number: project.forks_count, project: project.name) } do = sprite_icon('fork', size: 14, css_class: 'gl-mr-2') = badge_count(project.forks_count) - if show_count?(disabled: !merge_requests, compact_mode: compact_mode) - = link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do + = link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} merge request', '%{project} has %{number} merge requests', project.open_merge_requests_count), number: project.open_merge_requests_count, project: project.name) } do = sprite_icon('merge-request', size: 14, css_class: 'gl-mr-2') = badge_count(project.open_merge_requests_count) - if show_count?(disabled: !issues, compact_mode: compact_mode) - = link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do + = link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' }, aria: { label: format(n_('%{project} has %{number} open issues', '%{project} has %{number} open issues', project.open_issues_count), number: project.open_issues_count, project: project.name) } do = sprite_icon('issues', size: 14, css_class: 'gl-mr-2') = badge_count(project.open_issues_count) = render_if_exists 'shared/projects/actions', project: project diff --git a/config/feature_flags/beta/vue_merge_request_list.yml b/config/feature_flags/beta/vue_merge_request_list.yml deleted file mode 100644 index 0ebefadd4e1..00000000000 --- a/config/feature_flags/beta/vue_merge_request_list.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: vue_merge_request_list -feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/10827 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145168 -rollout_issue_url: -milestone: '16.10' -group: group::code review -type: beta -default_enabled: true diff --git a/config/feature_flags/gitlab_com_derisk/packages_protected_packages_delete.yml b/config/feature_flags/gitlab_com_derisk/packages_protected_packages_delete.yml new file mode 100644 index 00000000000..7911ee3ca2e --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/packages_protected_packages_delete.yml @@ -0,0 +1,9 @@ +--- +name: packages_protected_packages_delete +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323970 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179931 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/516215 +milestone: '17.10' +group: group::package registry +type: gitlab_com_derisk +default_enabled: false diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb index 9afb11d4b73..51721b51c33 100644 --- a/config/initializers/8_devise.rb +++ b/config/initializers/8_devise.rb @@ -116,15 +116,15 @@ Devise.setup do |config| # The time the user will be remembered without asking for credentials again. # config.remember_for = 2.weeks - # If true, a valid remember token can be re-used between multiple browsers. - # config.remember_across_browsers = true + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = false # If true, extends the user's remember period when remembered via cookie. config.extend_remember_period = true # Options to be passed to the created cookie. For instance, you can set # secure: true in order to force SSL only cookies. - # config.cookie_options = {} + # config.rememberable_options = {} # When set to false, does not sign a user in automatically after their password is # changed. Defaults to true, so a user is signed in automatically after a password diff --git a/config/initializers/validate_cell_config.rb b/config/initializers/validate_cell_config.rb index 9a43ecfe68d..5f92d1facc9 100644 --- a/config/initializers/validate_cell_config.rb +++ b/config/initializers/validate_cell_config.rb @@ -4,13 +4,24 @@ return if Gitlab::Utils.to_boolean(ENV['SKIP_CELL_CONFIG_VALIDATION'], default: ValidationError = Class.new(StandardError) +print_error = ->(error_message) do + message = error_message + message += <<~MESSAGE if Gitlab.dev_or_test_env? + + Make sure your development environment is up to date. + For example, on GDK, run: gdk update + MESSAGE + + raise ValidationError, message +end + if Gitlab.config.cell.enabled - raise ValidationError, "Cell ID is not set to a valid positive integer" if Gitlab.config.cell.id.to_i < 1 + print_error.call("Cell ID is not set to a valid positive integer.") if Gitlab.config.cell.id.to_i < 1 Settings.topology_service_settings.each do |setting| setting_value = Gitlab.config.cell.topology_service_client.send(setting) - raise ValidationError, "Topology Service setting '#{setting}' is not set" if setting_value.blank? + print_error.call("Topology Service setting '#{setting}' is not set.") if setting_value.blank? end elsif Gitlab.config.cell.id.present? - raise ValidationError, "Cell ID is set but Cell is not enabled" + print_error.call("Cell ID is set but Cell is not enabled.") end diff --git a/db/post_migrate/20250306065243_remove_broken_fk_a2141b1522_p.rb b/db/post_migrate/20250306065243_remove_broken_fk_a2141b1522_p.rb index 0eda63800f5..3665272f54c 100644 --- a/db/post_migrate/20250306065243_remove_broken_fk_a2141b1522_p.rb +++ b/db/post_migrate/20250306065243_remove_broken_fk_a2141b1522_p.rb @@ -1,41 +1,9 @@ # frozen_string_literal: true class RemoveBrokenFkA2141b1522P < Gitlab::Database::Migration[2.2] - include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers - milestone '17.10' - disable_ddl_transaction! - SOURCE_TABLE_NAME = :p_ci_builds - TARGET_TABLE_NAME = :p_ci_pipelines - COLUMN = :auto_canceled_by_id - TARGET_COLUMN = :id - PARTITION_COLUMN = :auto_canceled_by_partition_id - PARTITION_TARGET_COLUMN = :partition_id - FK_NAME = :fk_a2141b1522_p - - def up - with_lock_retries do - remove_foreign_key_if_exists( - SOURCE_TABLE_NAME, - TARGET_TABLE_NAME, - name: FK_NAME, - reverse_lock_order: true - ) - end - end - - def down - add_concurrent_partitioned_foreign_key( - SOURCE_TABLE_NAME, - TARGET_TABLE_NAME, - column: [PARTITION_COLUMN, COLUMN], - target_column: [PARTITION_TARGET_COLUMN, TARGET_COLUMN], - reverse_lock_order: true, - on_update: :cascade, - on_delete: :nullify, - name: FK_NAME, - validate: true - ) - end + # No-op for https://gitlab.com/gitlab-com/gl-infra/production/-/issues/19464 + def up; end + def down; end end diff --git a/db/structure.sql b/db/structure.sql index c96b5304fc3..24562a44176 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -40484,6 +40484,9 @@ ALTER TABLE ONLY ml_candidates ALTER TABLE ONLY subscription_add_on_purchases ADD CONSTRAINT fk_a1db288990 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE p_ci_builds + ADD CONSTRAINT fk_a2141b1522_p FOREIGN KEY (auto_canceled_by_partition_id, auto_canceled_by_id) REFERENCES p_ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE SET NULL; + ALTER TABLE ONLY protected_environment_approval_rules ADD CONSTRAINT fk_a3cc825836 FOREIGN KEY (protected_environment_project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 76c0c0cc893..0b9e1020bd8 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -40892,6 +40892,7 @@ AI features that can be configured in the settings. | `DUO_CHAT_EXPLAIN_CODE` | Duo chat explain code feature setting. | | `DUO_CHAT_FIX_CODE` | Duo chat fix code feature setting. | | `DUO_CHAT_REFACTOR_CODE` | Duo chat refactor code feature setting. | +| `DUO_CHAT_TROUBLESHOOT_JOB` | Duo chat troubleshoot job feature setting. | | `DUO_CHAT_WRITE_TESTS` | Duo chat write test feature setting. | ### `AiMessageRole` diff --git a/doc/api/packages.md b/doc/api/packages.md index dcbca62da2f..3186f0f1df2 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -433,12 +433,15 @@ curl --request DELETE --header "PRIVATE-TOKEN: " "https://git Can return the following status codes: -- `204 No Content`, if the package was deleted successfully. -- `404 Not Found`, if the package was not found. +- `204 No Content`: The package was deleted successfully. +- `403 Forbidden`: The package is protected from deletion. +- `404 Not Found`: The package was not found. If [request forwarding](../user/packages/package_registry/supported_functionality.md#forwarding-requests) is enabled, deleting a package can introduce a [dependency confusion risk](../user/packages/package_registry/supported_functionality.md#deleting-packages). +If a package is protected by a [protection rule](../user/packages/package_registry/package_protection_rules.md#protect-a-package), then deleting the package is forbidden. + ## Delete a package file {{< alert type="warning" >}} @@ -467,5 +470,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: " "https://git Can return the following status codes: - `204 No Content`: The package was deleted successfully. -- `403 Forbidden`: The user does not have permission to delete the file. +- `403 Forbidden`: The user does not have permission to delete the file or the package is protected from deletion. - `404 Not Found`: The package or package file was not found. + +If a package that a package file belongs to is protected by a [protection rule](../user/packages/package_registry/package_protection_rules.md#protect-a-package), then deleting the package file is forbidden. diff --git a/doc/ci/jobs/job_artifacts.md b/doc/ci/jobs/job_artifacts.md index 5336a98e5df..d09a7e8310b 100644 --- a/doc/ci/jobs/job_artifacts.md +++ b/doc/ci/jobs/job_artifacts.md @@ -282,6 +282,8 @@ build_submodule: - unzip artifacts.zip ``` +To fetch artifacts from a job in the same pipeline, use the [`needs:artifacts`](../yaml/_index.md#needsartifacts) keyword. + ## Browse the contents of the artifacts archive You can browse the contents of the artifacts from the UI without downloading the artifact locally, diff --git a/doc/ci/runners/configure_runners.md b/doc/ci/runners/configure_runners.md index 45c21b11a0b..6d22c98e7d3 100644 --- a/doc/ci/runners/configure_runners.md +++ b/doc/ci/runners/configure_runners.md @@ -739,8 +739,13 @@ subcommand. However, `GIT_SUBMODULE_UPDATE_FLAGS` flags are appended after a few Git honors the last occurrence of a flag in the list of arguments, so manually providing them in `GIT_SUBMODULE_UPDATE_FLAGS` overrides these default flags. -You can use this variable to fetch the latest remote `HEAD` instead of the tracked commit in the repository. -You can also use it to speed up the checkout by fetching submodules in multiple parallel jobs. +For example, you can use this variable to: + +- Fetch the latest remote `HEAD` instead of the tracked commit in the + repository (default) to automatically updated all submodules with the + `--remote` flag. +- Speed up the checkout by fetching submodules in multiple parallel jobs with + the `--jobs 4` flag. ```yaml variables: @@ -758,12 +763,35 @@ git submodule update --init --depth 50 --recursive --remote --jobs 4 {{< alert type="warning" >}} -You should be aware of the implications for the security, stability, and reproducibility of -your builds when using the `--remote` flag. In most cases, it is better to explicitly track -submodule commits as designed, and update them using an auto-remediation/dependency bot. +You should be aware of the implications for the security, stability, and +reproducibility of your builds when using the `--remote` flag. In most cases, +it is better to explicitly track submodule commits as designed, and update them +using an auto-remediation/dependency bot. + +The `--remote` flag is not required to check out submodules at their committed +revisions. Use this flag only when you want to automatically updated submodules +to their latest remote versions. {{< /alert >}} +The behavior of `--remote` depends on your Git version. Some Git versions might +fail, with the error below, when the branch in the superproject's `.gitmodules` +differs from the default branch of the submodule repository: + +`fatal: Unable to find refs/remotes/origin/ revision in submodule path ''` + +The runner implements a "best effort" fallback that attempts to +pull remote refs when the submodule update fails. + +If this fallback does not work with your Git version, try one of the following +workarounds: + +- Update the submodule repository's default branch to match the branch set in + `.gitmodules` in the superproject. +- Set `GIT_SUBMODULE_DEPTH` to `0`. +- Update the submodules separately and remove the `--remote` flag from + `GIT_SUBMODULE_UPDATE_FLAGS`. + ### Rewrite submodule URLs to HTTPS {{< history >}} diff --git a/doc/development/documentation/topic_types/_index.md b/doc/development/documentation/topic_types/_index.md index 79ed0caa16a..de2237df537 100644 --- a/doc/development/documentation/topic_types/_index.md +++ b/doc/development/documentation/topic_types/_index.md @@ -18,6 +18,10 @@ includes a task or reference topic. The tech writing team sometimes uses the acronym `CTRT` to refer to the topic types. The acronym refers to the first letter of each topic type. + +For an overview, see [Editing for style and topic type](https://youtu.be/HehnjPgPWb0). + + ## Other page and topic types In addition to the four primary topic types, you can use the following: diff --git a/doc/development/documentation/topic_types/concept.md b/doc/development/documentation/topic_types/concept.md index eeb2bf4c243..332d919b7cd 100644 --- a/doc/development/documentation/topic_types/concept.md +++ b/doc/development/documentation/topic_types/concept.md @@ -59,3 +59,24 @@ Avoid these topic titles: noun or phrase that someone would search for. - `Use cases`. Instead, incorporate the information as part of the concept. - `How it works`. Instead, use a noun followed by `workflow`. For example, `Merge request workflow`. + +## Example + +### Before + +The following topic was trying to be all things to all people. It provided information about groups +and where to find them. It reiterated what was visible in the UI. + +![An example concept and task](img/example_1.png) + +### After + +The information is easier to scan if you move it into concepts and [tasks](task.md). + +#### Concept + +![A concept example after it's been corrected](img/example_1_after_concept.png) + +#### Task + +![A task example after it's been corrected](img/example_1_after_task.png) diff --git a/doc/development/documentation/topic_types/img/example_1.png b/doc/development/documentation/topic_types/img/example_1.png new file mode 100644 index 00000000000..b12d6df44b9 Binary files /dev/null and b/doc/development/documentation/topic_types/img/example_1.png differ diff --git a/doc/development/documentation/topic_types/img/example_1_after_concept.png b/doc/development/documentation/topic_types/img/example_1_after_concept.png new file mode 100644 index 00000000000..fe1b8dfe31f Binary files /dev/null and b/doc/development/documentation/topic_types/img/example_1_after_concept.png differ diff --git a/doc/development/documentation/topic_types/img/example_1_after_task.png b/doc/development/documentation/topic_types/img/example_1_after_task.png new file mode 100644 index 00000000000..ede5de62dd8 Binary files /dev/null and b/doc/development/documentation/topic_types/img/example_1_after_task.png differ diff --git a/doc/development/documentation/topic_types/img/reference_example1.png b/doc/development/documentation/topic_types/img/reference_example1.png new file mode 100644 index 00000000000..4b421c62cd0 Binary files /dev/null and b/doc/development/documentation/topic_types/img/reference_example1.png differ diff --git a/doc/development/documentation/topic_types/img/reference_example2.png b/doc/development/documentation/topic_types/img/reference_example2.png new file mode 100644 index 00000000000..e026412e2e4 Binary files /dev/null and b/doc/development/documentation/topic_types/img/reference_example2.png differ diff --git a/doc/development/documentation/topic_types/reference.md b/doc/development/documentation/topic_types/reference.md index fcc5ef649a9..83200a1addc 100644 --- a/doc/development/documentation/topic_types/reference.md +++ b/doc/development/documentation/topic_types/reference.md @@ -34,3 +34,18 @@ Avoid these topic titles: for a task, or information about a concept. - `Limitations`. Instead, move the content near other similar information. If you must, you can use the title `Known issues`. + +## Example + +### Before + +This topic was a compilation of a variety of information and was difficult to scan. + +![An example of a reference topic](img/reference_example1.png) + +### After + +The information in the **Overview** topic is now organized in a table +that's easy to scan. It also has a more searchable title. + +![An example of a corrected reference topic](img/reference_example2.png) diff --git a/doc/development/documentation/topic_types/task.md b/doc/development/documentation/topic_types/task.md index b7617a43baf..5324e3ebef4 100644 --- a/doc/development/documentation/topic_types/task.md +++ b/doc/development/documentation/topic_types/task.md @@ -163,3 +163,4 @@ how to write the phrase for each role. ## Related topics - [How to write task steps](../styleguide/_index.md#navigation) +- [Before and after example](concept.md#example) diff --git a/doc/development/sql.md b/doc/development/sql.md index 65876eddb3a..89419611ead 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -435,14 +435,37 @@ scope2 = User.select(*columns).where(id: [10, 11, 12]) # uses SELECT users.* User.connection.execute(Gitlab::SQL::Union.new([scope1, scope2]).to_sql) ``` -## Ordering by Creation Date +## Ordering by Creation Date (`created_at`) -When ordering records based on the time they were created, you can order -by the `id` column instead of ordering by `created_at`. Because IDs are always -unique and incremented in the order that rows are created, doing so produces the -exact same results. This also means there's no need to add an index on -`created_at` to ensure consistent performance as `id` is already indexed by -default. +In short, you should prefer `ORDER BY id` over `ORDER BY created_at` unless you +are sure it is going to cause problems for your feature. + +There is a common user facing desire to provide data that is sorted by +`created_at`. It's common in paginated table views and paginated APIs to want +to see the most recent first (or oldest first). This usually results in us +wanting to add something like `ORDER BY created_at DESC LIMIT 20` to our +queries. Adding this query would mean that we need to add an index on +`created_at` (or composite index depending on the other filtering +requirements). Adding indexes comes with +[a cost](database/adding_database_indexes.md#maintenance-overhead). +Furthermore, since `created_at` usually isn't a unique column then sorting +and paginating over it would be unstable and we'd still need to add a +[tie-breaker column to the sort](database/pagination_performance_guidelines.md#tie-breaker-column) +(e.g. `ORDER BY created_at, id`) with an appropriate index for that. + +But, for the majority of features our users find that `ORDER BY id` is a good +enough proxy for what they need. It's not technically always +true that ordering by `id` is exactly the same as ordering by `created_at` but +it is close enough and considering that `created_at` is almost never controlled +directly by users (ie. it's an internal implementation detail), then there is +rarely a case where the user actually cares about the difference between these +2 columns. + +So there are at least 3 advantages to ordering by `id`: + +1. As a primary key, it is already indexed, which may be sufficient for simple queries that don't have other filtering or sorting parameters. +1. If a composite index is required, indexes such as `btree (namespace_id, id)` are smaller than `btree (namespace_id, created_at, id)`. +1. It is unique and thus stable for sorting and paginating. ## Use `WHERE EXISTS` instead of `WHERE IN` diff --git a/doc/user/application_security/dast/browser/configuration/customize_settings.md b/doc/user/application_security/dast/browser/configuration/customize_settings.md index 2dcb71a9311..21fc39eaa2b 100644 --- a/doc/user/application_security/dast/browser/configuration/customize_settings.md +++ b/doc/user/application_security/dast/browser/configuration/customize_settings.md @@ -145,3 +145,78 @@ dast: Adjusting these values may impact scan time because they adjust how long each browser waits for various activities to complete. {{< /alert >}} + +### Page readiness timeouts + +Page readiness refers to the state when a page has loaded completely, its DOM has stabilized, and interactive elements are available. Proper page readiness detection is crucial for: + +- **Scanning accuracy**: Analyzing pages before they're fully loaded can miss content or produce false negatives. +- **Crawl efficiency**: Waiting too long wastes scanning time, while not waiting enough misses dynamic content. +- **Modern web application support**: Single-page applications, AJAX-heavy sites, and progressive loading patterns require sophisticated readiness detection. + +Using a sequence of optional configurable timeouts, the DAST scanner can detect when different parts of a page have loaded completely. + +#### Timeout variables + +Use the following CI/CD variables to customize DAST page readiness timeouts. +For a comprehensive list, see [Available CI/CD variables](variables.md). + +| Timeout Variable | Default | Description | +|:-----------------|:--------|:------------| +| `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. Used during the Document Load phase for full page loads. | +| `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. Used as an alternative to `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` for in-page actions that don't trigger a full page load. | +| `DAST_PAGE_DOM_STABLE_WAIT` | `500ms` | Define how long to wait for updates to the DOM before checking a page is stable. Used at the beginning of the client-side render phase. | +| `DAST_PAGE_DOM_READY_TIMEOUT` | `6s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. Controls waiting for background data fetching and DOM rendering. | +| `DAST_PAGE_IS_LOADING_ELEMENT` | None | Selector that when no longer visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Marks the end of the client-side render process. | + +#### Page loading workflow + +Modern web applications load in multiple stages. The DAST scanner has specific timeouts for +each step in the process: + +1. **Document loading**: The browser fetches and processes the basic page structure. + + 1. Fetch HTML content from the server. + 1. Load referenced CSS and JavaScript files. + 1. Parse content and renders the initial page. + 1. Trigger the standard "document ready" event. + + This phase uses either `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` (for full page loads) or `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` (for in-page actions), which sets the maximum wait time for document loading. + +1. **Client-Side rendering**: After initial loading, many single-page applications: + + - Perform initial JavaScript execution (`DAST_PAGE_DOM_STABLE_WAIT`). + - Fetch background data with AJAX or other API calls. + - Render a DOM and performs updates based on fetched data (`DAST_PAGE_DOM_READY_TIMEOUT`). + - Display page loading indicators (`DAST_PAGE_IS_LOADING_ELEMENT`). + + The scanner monitors these activities to determine when the page is ready for interaction. + +The following chart illustrates the sequence timeouts used when crawling a page: + +```mermaid +%%{init: { + "gantt": { + "leftPadding": 250, + "sectionFontSize": 15, + "topPadding": 40, + "fontFamily": "GitLab Sans" + } +}}%% +gantt + dateFormat YYYY-MM-DD + axisFormat + section Document load + DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT :done, nav1, 2024-01-01, 6d + Fetch HTML :active, nav1, 2024-01-01, 3d + Fetch CSS&JS :active, nav1, 2024-01-04, 3d + DocumentReady :milestone, nav1, 2024-01-07, 0d + + section Load Data / Client-side render + DAST_PAGE_DOM_STABLE_WAIT :done, dom1, 2024-01-07, 3d + Initial JS Execution :active, dom1, 2024-01-07, 3d + DAST_PAGE_DOM_READY_TIMEOUT :done, ready1, 2024-01-10, 4d + Fetch Data :active, dom1, 2024-01-10, 2d + Render DOM :active, dom1, 2024-01-10, 2d + DAST_PAGE_IS_LOADING_ELEMENT :milestone, load1, 2024-01-14, 0d +``` diff --git a/doc/user/application_security/vulnerability_report/_index.md b/doc/user/application_security/vulnerability_report/_index.md index 002907bd013..fd2f89f7511 100644 --- a/doc/user/application_security/vulnerability_report/_index.md +++ b/doc/user/application_security/vulnerability_report/_index.md @@ -302,6 +302,7 @@ refreshed. {{< history >}} - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/16157) in GitLab 17.9 [with a flag](../../../administration/feature_flags.md) named `vulnerability_severity_override`. Disabled by default. +- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://issue-link) in GitLab 17.10. {{< /history >}} @@ -309,7 +310,6 @@ refreshed. The availability of this feature is controlled by a feature flag. For more information, see the history. -This feature is available for testing, but not ready for production use. {{< /alert >}} diff --git a/doc/user/gitlab_duo/choose_path.md b/doc/user/gitlab_duo/choose_path.md new file mode 100644 index 00000000000..9971f6124d7 --- /dev/null +++ b/doc/user/gitlab_duo/choose_path.md @@ -0,0 +1,169 @@ +--- +stage: AI-powered +group: AI Framework +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments +description: "Learn how to use GitLab Duo AI-powered features to enhance your software development lifecycle." +title: 'GitLab Duo: Choose your path' +--- + +GitLab Duo is a suite of AI-powered features that assist you while you work in GitLab. + +Select the path that best matches what you want to do: + +{{< tabs >}} + +{{< tab title="Get started" >}} + +**Perfect for**: New users exploring GitLab Duo + +Follow this path to learn how to: + +- Use the variety of GitLab Duo features +- Get help from AI through GitLab Duo Chat +- Generate and improve code + +[Start here: GitLab Duo →](_index.md) + +{{< /tab >}} + +{{< tab title="Enhance my coding" >}} + +**Perfect for**: Developers looking to boost productivity + +Follow this path to learn how to: + +- Use Code Suggestions in your IDE +- Generate, understand, and refactor code +- Create tests automatically + +[Start here: Code Suggestions →](../../user/project/repository/code_suggestions/_index.md) + +{{< /tab >}} + +{{< tab title="Improve code reviews" >}} + +**Perfect for**: Reviewers and team leads + +Follow this path to learn how to: + +- Generate merge request descriptions +- Get AI-powered code reviews +- Summarize review comments and generate commit messages + +[Start here: GitLab Duo in merge requests →](../../user/project/merge_requests/duo_in_merge_requests.md) + +{{< /tab >}} + +{{< tab title="Secure my application" >}} + +**Perfect for**: Security and DevSecOps professionals + +Follow this path to learn how to: + +- Understand vulnerabilities +- Automatically generate fix suggestions +- Create merge requests to address security issues + +[Start here: Vulnerability explanation and resolution →](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability) + +{{< /tab >}} + +{{< /tabs >}} + +## Quick start + +Want to start using GitLab Duo right now? Here's how: + +1. Open GitLab Duo Chat by selecting **GitLab Duo Chat** in the upper-right corner of the GitLab UI. +1. Ask a question about your project, code, or how to use GitLab. +1. Try one of the AI-powered features like Code Suggestions in your IDE, or use Chat to summarize a bulky issue. + +[View all of the GitLab Duo possibilities →](_index.md) + +## Common tasks + +Need to do something specific? Here are some common tasks: + +| Task | Description | Quick Guide | +|------|-------------|-------------| +| Get AI assistance | Ask GitLab Duo questions about code, projects, or GitLab | [GitLab Duo Chat →](../gitlab_duo_chat/_index.md) | +| Generate code | Get code suggestions as you type in your IDE | [Code Suggestions →](../../user/project/repository/code_suggestions/_index.md) | +| Understand code | Have code explained in plain language | [Code Explanation →](../../user/project/repository/code_explain.md) | +| Fix CI/CD issues | Analyze and fix failed jobs | [Root Cause Analysis →](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | +| Summarize changes | Generate descriptions for merge requests | [Merge Request Summary →](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) | + +## How GitLab Duo integrates with your workflow + +GitLab Duo is integrated with your development processes and is available: + +- In the GitLab UI +- Through GitLab Duo Chat +- In IDE extensions +- In the CLI + +## Experience levels + +### For beginners + +If you're new to GitLab Duo, start with these features: + +- **[GitLab Duo Chat](../gitlab_duo_chat/_index.md)** - Ask questions about GitLab and get help with basic tasks +- **[Code Explanation](../../user/project/repository/code_explain.md)** - Understand code in files or merge requests +- **[Merge Request Summary](../../user/project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes)** - Generate descriptions for your changes automatically + +### For intermediate users + +After you're comfortable with the basics, try these more advanced features: + +- **[Code Suggestions](../../user/project/repository/code_suggestions/_index.md)** - Get AI-powered code completion in your IDE +- **[Test Generation](../gitlab_duo_chat/examples.md#write-tests-in-the-ide)** - Create tests for your code automatically +- **[Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis)** - Troubleshoot failed CI/CD jobs + +### For advanced users + +When you're ready to maximize your productivity with GitLab Duo: + +- **[GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md)** - Host LLMs on your own infrastructure +- **[GitLab Duo Workflow](../duo_workflow/_index.md)** - Automate tasks in your development workflow +- **[Vulnerability Resolution](../../user/application_security/vulnerabilities/_index.md#vulnerability-resolution)** - Automatically generate merge requests to fix security issues + +## Best practices + +Follow these tips for effective GitLab Duo usage: + +1. **Be specific in your prompts** + - Provide clear context for better results + - Include relevant details about your code and objectives + - Use code task commands like `/explain`, `/refactor`, and `/tests` in Chat + +1. **Improve code responsibly** + - Always review AI-generated code before using it + - Test generated code to ensure it works as expected + - Use vulnerability resolution with appropriate review + +1. **Refine iteratively** + - If a response isn't helpful, refine your question + - Try breaking complex requests into smaller parts + - Add more details for better context + +1. **Leverage Chat for learning** + - Ask about GitLab features you're not familiar with + - Get explanations for error messages and problems + - Learn best practices for your specific technology + +## Next steps + +Ready to dive deeper? Try these resources: + +- [GitLab Duo use cases](use_cases.md) - Practical examples and exercises +- [Set up GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md) - For complete control over your data + +## Troubleshooting + +Having issues? Check these common solutions: + +- [GitLab Duo features don't work on self-managed](troubleshooting.md#gitlab-duo-features-do-not-work-on-self-managed) +- [GitLab Duo features not available for users](troubleshooting.md#gitlab-duo-features-not-available-for-users) +- [Run a health check](setup.md#run-a-health-check-for-gitlab-duo) to diagnose your GitLab Duo setup + +Need more help? Search the GitLab documentation or [ask the GitLab community](https://forum.gitlab.com/). diff --git a/lib/api/helpers/personal_access_tokens_helpers.rb b/lib/api/helpers/personal_access_tokens_helpers.rb index b1057987604..cab5ec02e1f 100644 --- a/lib/api/helpers/personal_access_tokens_helpers.rb +++ b/lib/api/helpers/personal_access_tokens_helpers.rb @@ -18,6 +18,10 @@ module API documentation: { example: '2021-01-01' } optional :last_used_after, type: DateTime, desc: 'Filter tokens which were used after given datetime', documentation: { example: '2022-01-01' } + optional :expires_before, type: Date, desc: 'Filter tokens which expire before given datetime', + documentation: { example: '2022-01-01' } + optional :expires_after, type: Date, desc: 'Filter tokens which expire after given datetime', + documentation: { example: '2021-01-01' } optional :search, type: String, desc: 'Filters tokens by name', documentation: { example: 'token' } optional :sort, type: String, desc: 'Sort tokens', documentation: { example: 'created_at_desc' } end diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb index 9f9ad173d32..65eeb50c604 100644 --- a/lib/api/package_files.rb +++ b/lib/api/package_files.rb @@ -67,6 +67,17 @@ module API not_found! unless package + if Feature.enabled?(:packages_protected_packages_delete, user_project) + service_response = + Packages::Protection::CheckDeleteRuleExistenceService.new( + project: user_project, + current_user: current_user, + params: { package_name: package.name, package_type: package.package_type } + ).execute + + forbidden!('Package is deletion protected.') if service_response[:protection_rule_exists?] + end + package_file = package.installable_package_files .find_by_id(params[:package_file_id]) diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 99d9658d9d5..cc845deb595 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -139,6 +139,17 @@ module API delete ':id/packages/:package_id' do authorize_destroy_package!(user_project) + if Feature.enabled?(:packages_protected_packages_delete, user_project) + service_response = + Packages::Protection::CheckDeleteRuleExistenceService.new( + project: user_project, + current_user: current_user, + params: { package_name: package.name, package_type: package.package_type } + ).execute + + forbidden!('Package is deletion protected.') if service_response[:protection_rule_exists?] + end + destroy_conditionally!(package) do |package| ::Packages::MarkPackageForDestructionService.new(container: package, current_user: current_user).execute end diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb index 30b5d183989..161a68a1bcc 100644 --- a/lib/api/resource_access_tokens.rb +++ b/lib/api/resource_access_tokens.rb @@ -10,6 +10,8 @@ module API feature_category :system_access + helpers ::API::Helpers::PersonalAccessTokensHelpers + %w[project group].each do |source_type| resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get list of all access tokens for the specified resource' do @@ -20,15 +22,14 @@ module API end params do requires :id, types: [String, Integer], desc: "ID or URL-encoded path of the #{source_type}" - optional :state, type: String, desc: 'Filter tokens which are either active or inactive', - values: %w[active inactive], documentation: { example: 'active' } + use :access_token_params end get ":id/access_tokens" do resource = find_source(source_type, params[:id]) next unauthorized! unless current_user.can?(:read_resource_access_tokens, resource) - tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false, state: params[:state] }).execute.preload_users + tokens = PersonalAccessTokensFinder.new(declared(params, include_missing: false).merge({ user: resource.bots, impersonation: false })).execute.preload_users resource.members.load present paginate(tokens), with: Entities::ResourceAccessToken, resource: resource diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb index 7413813377f..0d4bf395180 100644 --- a/lib/gitlab/internal_events.rb +++ b/lib/gitlab/internal_events.rb @@ -25,6 +25,8 @@ module Gitlab track_analytics_event(event_name, send_snowplow_event, category: category, additional_properties: additional_properties, **kwargs) + return unless event_definition + kwargs[:additional_properties] = additional_properties event_definition.extra_tracking_classes.each do |tracking_class| tracking_class.track_event(event_name, **kwargs) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 95118ea64c8..ea82ab0801c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -36962,9 +36962,32 @@ msgstr "" msgid "MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile" msgstr "" +msgid "MlExperimentTracking|%d experiment" +msgid_plural "MlExperimentTracking|%d experiments" +msgstr[0] "" +msgstr[1] "" + +msgid "MlExperimentTracking|(Optional)" +msgstr "" + +msgid "MlExperimentTracking|Artifacts" +msgstr "" + +msgid "MlExperimentTracking|CI Info" +msgstr "" + +msgid "MlExperimentTracking|Complete the form below to promote run to a model version." +msgstr "" + +msgid "MlExperimentTracking|Create" +msgstr "" + msgid "MlExperimentTracking|Create an experiment using MLflow" msgstr "" +msgid "MlExperimentTracking|Create experiments using MLflow" +msgstr "" + msgid "MlExperimentTracking|Creating an experiment" msgstr "" @@ -36989,9 +37012,24 @@ msgstr "" msgid "MlExperimentTracking|Deleting this run will delete the associated parameters, metrics, and metadata." msgstr "" +msgid "MlExperimentTracking|Description" +msgstr "" + +msgid "MlExperimentTracking|Details & Metadata" +msgstr "" + msgid "MlExperimentTracking|Download as CSV" msgstr "" +msgid "MlExperimentTracking|Enter a model version description" +msgstr "" + +msgid "MlExperimentTracking|Enter a semantic version." +msgstr "" + +msgid "MlExperimentTracking|Enter some description" +msgstr "" + msgid "MlExperimentTracking|Experiment created %{timeAgo} by %{author}" msgstr "" @@ -37007,9 +37045,15 @@ msgstr "" msgid "MlExperimentTracking|Failed to load experiment candidates with error: %{message}" msgstr "" +msgid "MlExperimentTracking|Failed to load experiments with error: %{error}" +msgstr "" + msgid "MlExperimentTracking|Failed to remove run" msgstr "" +msgid "MlExperimentTracking|For example 1.0.0" +msgstr "" + msgid "MlExperimentTracking|Get started with model experiments!" msgstr "" @@ -37019,39 +37063,93 @@ msgstr "" msgid "MlExperimentTracking|Last activity" msgstr "" +msgid "MlExperimentTracking|MLflow run ID" +msgstr "" + +msgid "MlExperimentTracking|Metric" +msgstr "" + +msgid "MlExperimentTracking|Model" +msgstr "" + msgid "MlExperimentTracking|Model experiments" msgstr "" +msgid "MlExperimentTracking|Must be a semantic version." +msgstr "" + +msgid "MlExperimentTracking|Must be a semantic version. Latest version is %{latestVersion}" +msgstr "" + msgid "MlExperimentTracking|Name" msgstr "" msgid "MlExperimentTracking|No candidates associated with this experiment" msgstr "" +msgid "MlExperimentTracking|No logged artifacts." +msgstr "" + msgid "MlExperimentTracking|No logged experiment metadata" msgstr "" +msgid "MlExperimentTracking|No logged metadata" +msgstr "" + +msgid "MlExperimentTracking|No logged metrics" +msgstr "" + +msgid "MlExperimentTracking|No logged parameters" +msgstr "" + +msgid "MlExperimentTracking|No results" +msgstr "" + msgid "MlExperimentTracking|Number of runs" msgstr "" msgid "MlExperimentTracking|Overview" msgstr "" +msgid "MlExperimentTracking|Parameters" +msgstr "" + msgid "MlExperimentTracking|Performance" msgstr "" +msgid "MlExperimentTracking|Promote" +msgstr "" + +msgid "MlExperimentTracking|Promote run" +msgstr "" + msgid "MlExperimentTracking|Run %{id}" msgstr "" +msgid "MlExperimentTracking|Run not linked to a CI build" +msgstr "" + msgid "MlExperimentTracking|Run removed" msgstr "" msgid "MlExperimentTracking|Runs" msgstr "" +msgid "MlExperimentTracking|Select a model" +msgstr "" + +msgid "MlExperimentTracking|Select the model that will contain the new version. The run will move to the default experiment of that model." +msgstr "" + +msgid "MlExperimentTracking|Step %{step}" +msgstr "" + msgid "MlExperimentTracking|To learn more about MLflow client compatibility, see %{linkStart}the documentation%{linkEnd}." msgstr "" +msgid "MlExperimentTracking|Triggered by" +msgstr "" + msgid "MlExperimentTracking|Use candidates to track performance, parameters, and metadata" msgstr "" @@ -37061,13 +37159,17 @@ msgstr "" msgid "MlExperimentTracking|Value" msgstr "" -msgid "MlExperimentTracking|by %{author}" +msgid "MlExperimentTracking|Version" msgstr "" -msgid "MlModelRegistry|%d experiment" -msgid_plural "MlModelRegistry|%d experiments" -msgstr[0] "" -msgstr[1] "" +msgid "MlExperimentTracking|Version is not a valid semantic version." +msgstr "" + +msgid "MlExperimentTracking|Version is valid semantic version." +msgstr "" + +msgid "MlExperimentTracking|by %{author}" +msgstr "" msgid "MlModelRegistry|%d model" msgid_plural "MlModelRegistry|%d models" @@ -37109,18 +37211,12 @@ msgstr "" msgid "MlModelRegistry|CI Info" msgstr "" -msgid "MlModelRegistry|Complete the form below to promote run to a model version." -msgstr "" - msgid "MlModelRegistry|Create" msgstr "" msgid "MlModelRegistry|Create & import" msgstr "" -msgid "MlModelRegistry|Create experiments using MLflow" -msgstr "" - msgid "MlModelRegistry|Create model" msgstr "" @@ -37178,9 +37274,6 @@ msgstr "" msgid "MlModelRegistry|Description" msgstr "" -msgid "MlModelRegistry|Details & Metadata" -msgstr "" - msgid "MlModelRegistry|Drop or %{linkStart}select%{linkEnd} artifacts to attach" msgstr "" @@ -37223,9 +37316,6 @@ msgstr "" msgid "MlModelRegistry|Failed to delete model with error: %{message}" msgstr "" -msgid "MlModelRegistry|Failed to load experiments with error: %{error}" -msgstr "" - msgid "MlModelRegistry|Failed to load model runs with error: %{message}" msgstr "" @@ -37274,12 +37364,6 @@ msgstr "" msgid "MlModelRegistry|Metadata" msgstr "" -msgid "MlModelRegistry|Metric" -msgstr "" - -msgid "MlModelRegistry|Model" -msgstr "" - msgid "MlModelRegistry|Model card" msgstr "" @@ -37325,9 +37409,6 @@ msgstr "" msgid "MlModelRegistry|No description available. To add a description, click \"Edit model\" above." msgstr "" -msgid "MlModelRegistry|No logged artifacts." -msgstr "" - msgid "MlModelRegistry|No logged metadata" msgstr "" @@ -37337,9 +37418,6 @@ msgstr "" msgid "MlModelRegistry|No logged parameters" msgstr "" -msgid "MlModelRegistry|No results" -msgstr "" - msgid "MlModelRegistry|No runs associated with this model" msgstr "" @@ -37352,12 +37430,6 @@ msgstr "" msgid "MlModelRegistry|Performance" msgstr "" -msgid "MlModelRegistry|Promote" -msgstr "" - -msgid "MlModelRegistry|Promote run" -msgstr "" - msgid "MlModelRegistry|Provide a subfolder name to organize your artifacts. Entering an existing subfolder's name will place artifacts in the existing folder" msgstr "" @@ -37376,21 +37448,12 @@ msgstr "" msgid "MlModelRegistry|Save changes" msgstr "" -msgid "MlModelRegistry|Select a model" -msgstr "" - -msgid "MlModelRegistry|Select the model that will contain the new version. The run will move to the default experiment of that model." -msgstr "" - msgid "MlModelRegistry|Setting up the client" msgstr "" msgid "MlModelRegistry|Something went wrong while trying to delete the model version. Please try again later." msgstr "" -msgid "MlModelRegistry|Step %{step}" -msgstr "" - msgid "MlModelRegistry|Subfolder" msgstr "" @@ -43467,7 +43530,7 @@ msgstr "" msgid "Pipeline|Only the first 100 jobs per stage are displayed" msgstr "" -msgid "Pipeline|Other" +msgid "Pipeline|Other (Cancelled, Skipped)" msgstr "" msgid "Pipeline|Passed" diff --git a/package.json b/package.json index 5fdc1bc2760..ccfc5b2260a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@gitlab/fonts": "^1.3.0", "@gitlab/query-language-rust": "0.4.0", "@gitlab/svgs": "3.123.0", - "@gitlab/ui": "110.0.0", + "@gitlab/ui": "110.1.0", "@gitlab/vue-router-vue3": "npm:vue-router@4.5.0", "@gitlab/vuex-vue3": "npm:vuex@4.1.0", "@gitlab/web-ide": "^0.0.1-dev-20250309164831", diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt index 925d254425a..0c6343a2682 100644 --- a/scripts/frontend/quarantined_vue3_specs.txt +++ b/scripts/frontend/quarantined_vue3_specs.txt @@ -195,7 +195,6 @@ spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.j spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js spec/frontend/ref/init_ambiguous_ref_modal_spec.js -spec/frontend/releases/components/app_edit_new_spec.js spec/frontend/releases/components/asset_links_form_spec.js spec/frontend/repository/components/table/index_spec.js spec/frontend/repository/components/table/row_spec.js diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 251e6153f64..7a98c8eae04 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -523,92 +523,6 @@ RSpec.describe GroupsController, :with_current_organization, factory_default: :k expect(response).to render_template('groups/merge_requests') end - - context 'sorting by votes' do - context 'when vue_merge_request_list is disabled' do - before do - stub_feature_flags(vue_merge_request_list: false) - end - - it 'sorts most popular merge requests' do - get :merge_requests, params: { id: group.to_param, sort: 'upvotes_desc' } - expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1] - end - - it 'sorts least popular merge requests' do - get :merge_requests, params: { id: group.to_param, sort: 'downvotes_desc' } - expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1] - end - end - end - - context 'rendering views' do - before do - stub_feature_flags(vue_merge_request_list: false) - end - - render_views - - it 'displays MR counts in nav' do - get :merge_requests, params: { id: group.to_param } - - expect(response.body).to have_content('Open 2 Merged 0 Closed 0 All 2') - expect(response.body).not_to have_content('Open Merged Closed All') - end - - context 'when MergeRequestsFinder raises an exception' do - before do - allow_next_instance_of(MergeRequestsFinder) do |instance| - allow(instance).to receive(:count_by_state).and_raise(ActiveRecord::QueryCanceled) - end - end - - it 'does not display MR counts in nav' do - get :merge_requests, params: { id: group.to_param } - - expect(response.body).to have_content('Open Merged Closed All') - expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0') - end - end - end - - context 'when an ActiveRecord::QueryCanceled is raised' do - before do - stub_feature_flags(vue_merge_request_list: false) - - allow_next_instance_of(Gitlab::IssuableMetadata) do |instance| - allow(instance).to receive(:data).and_raise(ActiveRecord::QueryCanceled) - end - end - - it 'sets :search_timeout_occurred' do - get :merge_requests, params: { id: group.to_param } - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:search_timeout_occurred)).to eq(true) - end - - it 'logs the exception' do - get :merge_requests, params: { id: group.to_param } - end - - context 'rendering views' do - render_views - - it 'shows error message' do - get :merge_requests, params: { id: group.to_param } - - expect(response.body).to have_content('Too many results to display. Edit your search or add a filter.') - end - - it 'does not display MR counts in nav' do - get :merge_requests, params: { id: group.to_param } - - expect(response.body).to have_content('Open Merged Closed All') - expect(response.body).not_to have_content('Open 0 Merged 0 Closed 0 All 0') - end - end - end end describe 'DELETE #destroy' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 795f0052444..6856f06a97e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -392,77 +392,6 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review expect(response).to render_template('projects/merge_requests/index') end - - context 'when vue_merge_request_list is disabled' do - before do - stub_feature_flags(vue_merge_request_list: false) - end - - context 'when the test is flaky', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/450217' do - it_behaves_like "issuables list meta-data", :merge_request - end - - it_behaves_like 'set sort order from user preference' do - let(:sorting_param) { 'updated_asc' } - end - - context 'when page param' do - let(:last_page) { project.merge_requests.page.total_pages } - let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } - - it 'redirects to last_page if page number is larger than number of pages' do - get_merge_requests(last_page + 1) - - expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope])) - end - - it 'redirects to specified page' do - get_merge_requests(last_page) - - expect(assigns(:merge_requests).current_page).to eq(last_page) - expect(response).to have_gitlab_http_status(:ok) - end - - it 'does not redirect to external sites when provided a host field' do - external_host = "www.example.com" - get :index, - params: { - namespace_id: project.namespace.to_param, - project_id: project, - state: 'opened', - page: (last_page + 1).to_param, - host: external_host - } - - expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope])) - end - end - - context 'when filtering by opened state' do - context 'with opened merge requests' do - it 'lists those merge requests' do - expect(merge_request).to be_persisted - - get_merge_requests - - expect(assigns(:merge_requests)).to include(merge_request) - end - end - - context 'with reopened merge requests' do - before do - merge_request.close! - merge_request.reopen! - end - - it 'lists those merge requests' do - get_merge_requests - - expect(assigns(:merge_requests)).to include(merge_request) - end - end - end - end end describe 'PUT update' do diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb index 066c6b3c7da..1c67c688158 100644 --- a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb +++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb @@ -47,24 +47,5 @@ RSpec.describe 'Groups > User sees users dropdowns in issuables list', :js, feat end end end - - context 'when vue_merge_request_list feature flag is disabled' do - before do - stub_feature_flags(vue_merge_request_list: false) - end - - %w[author assignee].each do |dropdown| - describe "#{dropdown} dropdown" do - it 'only includes members of the project/group' do - visit merge_requests_group_path(group) - - filtered_search.set("#{dropdown}:=") - - expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name) - expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name) - end - end - end - end end end diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js index 8ddf8390431..8196a08f5b4 100644 --- a/spec/frontend/filtered_search/dropdown_user_spec.js +++ b/spec/frontend/filtered_search/dropdown_user_spec.js @@ -1,4 +1,4 @@ -import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html'; +import htmlMergeRequestList from 'test_fixtures_static/merge_request_list.html'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import DropdownUser from '~/filtered_search/dropdown_user'; import DropdownUtils from '~/filtered_search/dropdown_utils'; diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js index d8a5b493b7a..776601ab04e 100644 --- a/spec/frontend/filtered_search/dropdown_utils_spec.js +++ b/spec/frontend/filtered_search/dropdown_utils_spec.js @@ -1,4 +1,4 @@ -import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html'; +import htmlMergeRequestList from 'test_fixtures_static/merge_request_list.html'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper'; import DropdownUtils from '~/filtered_search/dropdown_utils'; diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index 6e3c779396d..5fd24eb67a9 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -127,19 +127,6 @@ RSpec end end - it 'merge_requests/merge_request_list.html' do - stub_feature_flags(vue_merge_request_list: false) - - create(:merge_request, source_project: project, target_project: project) - - get :index, params: { - namespace_id: project.namespace.to_param, - project_id: project - } - - expect(response).to be_successful - end - describe GraphQL::Query, type: :request do include ApiHelpers include GraphqlHelpers diff --git a/spec/frontend/fixtures/static/merge_request_list.html b/spec/frontend/fixtures/static/merge_request_list.html new file mode 100644 index 00000000000..5fbcb4159fd --- /dev/null +++ b/spec/frontend/fixtures/static/merge_request_list.html @@ -0,0 +1,1260 @@ + + + + + Merge requests · Sidney Jones42 / Merge-requests-project Name · GitLab + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+ +
+ + +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ + + + +
+
+
+
+
+ + + +
+ +
+
+
+
+
+ diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js index c649aaa60aa..3ffb30366c7 100644 --- a/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js @@ -36,7 +36,7 @@ describe('PipelineStatusChart', () => { bars: [ { data: [], name: 'Successful' }, { data: [], name: 'Failed' }, - { data: [], name: 'Other' }, + { data: [], name: 'Other (Cancelled, Skipped)' }, ], groupBy: [], customPalette: ['#619025', '#b93d71', '#617ae2'], @@ -61,7 +61,7 @@ describe('PipelineStatusChart', () => { expect(findStackedColumnChart().props('bars')).toEqual([ { data: [10, 11], name: 'Successful' }, { data: [20, 21], name: 'Failed' }, - { data: [30, 31], name: 'Other' }, + { data: [30, 31], name: 'Other (Cancelled, Skipped)' }, ]); }); diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index 6460209bb38..8215b255da0 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -7,6 +7,7 @@ import { nextTick } from 'vue'; import { GlDatepicker, GlFormCheckbox } from '@gitlab/ui'; import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import createMilestoneComboboxState from '~/milestones/stores/state'; import { convertOneReleaseGraphQLResponse } from '~/releases/util'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; @@ -86,6 +87,25 @@ describe('Release edit/new component', () => { merge( { modules: { + milestoneCombobox: { + namespaced: true, + actions: { + setProjectId: jest.fn(), + setGroupId: jest.fn(), + setGroupMilestonesAvailable: jest.fn(), + setSelectedMilestones: jest.fn(), + clearSelectedMilestones: jest.fn(), + toggleMilestones: jest.fn(), + search: jest.fn(), + fetchMilestones: jest.fn(), + fetchProjectMilestones: jest.fn(), + fetchGroupMilestones: jest.fn(), + searchProjectMilestones: jest.fn(), + searchGroupMilestones: jest.fn(), + }, + state: createMilestoneComboboxState(), + getters: { isLoading: jest.fn() }, + }, editNew: { namespaced: true, actions, @@ -239,6 +259,7 @@ describe('Release edit/new component', () => { store: { modules: { editNew: { + namespaced: true, state: { isExistingRelease: false }, }, }, @@ -280,6 +301,7 @@ describe('Release edit/new component', () => { store: { modules: { editNew: { + namespaced: true, getters: { isValid: () => true, }, @@ -300,6 +322,7 @@ describe('Release edit/new component', () => { store: { modules: { editNew: { + namespaced: true, getters: { isValid: () => false, }, @@ -326,6 +349,7 @@ describe('Release edit/new component', () => { store: { modules: { editNew: { + namespaced: true, state: { isFetchingTagNotes: true, }, @@ -355,6 +379,7 @@ describe('Release edit/new component', () => { store: { modules: { editNew: { + namespaced: true, state: { isExistingRelease: false, }, diff --git a/spec/initializers/validate_cell_config_spec.rb b/spec/initializers/validate_cell_config_spec.rb index 7942cb8f8df..ebc4f8d2ff2 100644 --- a/spec/initializers/validate_cell_config_spec.rb +++ b/spec/initializers/validate_cell_config_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe 'validate database config', feature_category: :cell do include StubENV + let(:dev_message) do + "\nMake sure your development environment is up to date.\nFor example, on GDK, run: gdk update\n" + end + let(:rails_configuration) { Rails::Application::Configuration.new(Rails.root) } let(:valid_topology_service_client_config) do { @@ -71,22 +75,32 @@ RSpec.describe 'validate database config', feature_category: :cell do end it 'raises an exception' do - expect { validate_config }.to raise_error("Cell ID is set but Cell is not enabled") + expect { validate_config }.to raise_error("Cell ID is set but Cell is not enabled.#{dev_message}") end end end - context 'when configuration is wrong' do + context 'when configuration is invalid' do context 'when cell is enabled by cell id is not set' do before do stub_config(cell: { enabled: true, id: nil, topology_service_client: valid_topology_service_client_config }) end it 'raises exception about missing cell id' do - expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer") + expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.#{dev_message}") end it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true' + + context 'when not dev environment' do + before do + stub_rails_env('production') + end + + it 'raises exception about missing cell id' do + expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.") + end + end end context 'when cell is enabled by cell id is not valid' do @@ -95,7 +109,7 @@ RSpec.describe 'validate database config', feature_category: :cell do end it 'raises exception about missing cell id' do - expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer") + expect { validate_config }.to raise_error("Cell ID is not set to a valid positive integer.#{dev_message}") end it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true' @@ -107,7 +121,7 @@ RSpec.describe 'validate database config', feature_category: :cell do end it 'raises exception about missing topology service client config' do - expect { validate_config }.to raise_error("Topology Service setting 'address' is not set") + expect { validate_config }.to raise_error("Topology Service setting 'address' is not set.#{dev_message}") end it_behaves_like 'with SKIP_CELL_CONFIG_VALIDATION=true' diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb index 4f3c7218558..4f377f8989a 100644 --- a/spec/lib/gitlab/internal_events_spec.rb +++ b/spec/lib/gitlab/internal_events_spec.rb @@ -770,6 +770,20 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana allow(event_definition).to receive(:extra_tracking_classes).and_return([custom_tracking_class]) end + context 'when event is not defined' do + let(:event_name) { 'an_event_that_does_not_exist' } + + before do + allow(Gitlab::Tracking::EventDefinition).to receive(:find).with(event_name).and_return(nil) + end + + it 'does not call custom classes' do + expect(custom_tracking_class).not_to receive(:track_event) + + described_class.track_event(event_name, user: user, project: project) + end + end + it 'calls the custom classes' do expect(custom_tracking_class).to receive(:track_event).with(event_name, **event_kwargs) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e15af81ff90..1588bdd7b86 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2624,7 +2624,7 @@ RSpec.describe User, feature_category: :user_profile do end end - describe '#forget_me!' do + describe '#invalidate_all_remember_tokens!' do let(:user) { create(:user) } context 'when remember me application setting is disabled' do @@ -2638,7 +2638,7 @@ RSpec.describe User, feature_category: :user_profile do expect(user.remember_created_at).not_to be_nil stub_application_setting(remember_me_enabled: false) - user.forget_me! + user.invalidate_all_remember_tokens! expect(user.remember_created_at).to be_nil end diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb index 301e2cd81bf..602061a56ae 100644 --- a/spec/requests/api/package_files_spec.rb +++ b/spec/requests/api/package_files_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe API::PackageFiles, feature_category: :package_registry do let(:user) { create(:user) } - let(:project) { create(:project, :public) } + let_it_be(:project) { create(:project, :public) } let(:package) { create(:maven_package, project: project) } describe 'GET /projects/:id/packages/:package_id/package_files' do @@ -274,5 +274,112 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do end end end + + context 'with package protection rule for different roles and package_name_patterns', :enable_admin_mode do + using RSpec::Parameterized::TableSyntax + + let_it_be(:pat_project_maintainer) do + create(:personal_access_token, user: create(:user, maintainer_of: [project])) + end + + let_it_be(:pat_project_owner) { create(:personal_access_token, user: create(:user, owner_of: [project])) } + let_it_be(:pat_instance_admin) { create(:personal_access_token, :admin_mode, user: create(:admin)) } + let_it_be(:headers_pat_project_maintainer) do + { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_maintainer.token } + end + + let_it_be(:headers_pat_project_owner) do + { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_owner.token } + end + + let_it_be(:headers_pat_instance_admin) do + { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_instance_admin.token } + end + + let_it_be(:job_from_project_maintainer) do + create(:ci_build, :running, user: pat_project_maintainer.user, project: project) + end + + let_it_be(:job_from_project_owner) { create(:ci_build, :running, user: pat_project_owner.user, project: project) } + let(:headers_job_token_from_maintainer) do + { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_maintainer.token } + end + + let(:headers_job_token_from_owner) do + { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_owner.token } + end + + let(:package_protection_rule) { create(:package_protection_rule, project: project) } + + let(:package_name) { package.name } + let(:package_name_no_match) { "#{package_name}_no_match" } + + subject do + delete api(url), headers: headers + response + end + + shared_examples 'deleting package protected' do + it_behaves_like 'returning response status', :forbidden + it 'responds with correct error message' do + subject + + expect(json_response).to include('message' => "403 Forbidden - Package is deletion protected.") + end + + it { expect { subject }.not_to change { ::Packages::Package.pending_destruction.count } } + + context 'when feature flag :packages_protected_packages_delete disabled' do + before do + stub_feature_flags(packages_protected_packages_delete: false) + end + + it_behaves_like 'deleting package' + end + end + + shared_examples 'deleting package' do + it_behaves_like 'returning response status', :no_content + it { expect { subject }.to change { package.package_files.pending_destruction.count }.by(1) } + end + + where(:package_name_pattern, :minimum_access_level_for_delete, :headers, :shared_examples_name) do + ref(:package_name) | :owner | ref(:headers_job_token_from_maintainer) | 'deleting package protected' + ref(:package_name) | :owner | ref(:headers_job_token_from_owner) | 'deleting package' + ref(:package_name) | :owner | ref(:headers_pat_project_maintainer) | 'deleting package protected' + ref(:package_name) | :owner | ref(:headers_pat_project_owner) | 'deleting package' + ref(:package_name) | :owner | ref(:headers_pat_instance_admin) | 'deleting package' + + ref(:package_name) | :admin | ref(:headers_pat_project_maintainer) | 'deleting package protected' + ref(:package_name) | :admin | ref(:headers_pat_project_owner) | 'deleting package protected' + ref(:package_name) | :admin | ref(:headers_job_token_from_owner) | 'deleting package protected' + ref(:package_name) | :admin | ref(:headers_pat_instance_admin) | 'deleting package' + + ref(:package_name_no_match) | :owner | ref(:headers_pat_project_owner) | 'deleting package' + end + + with_them do + before do + package_protection_rule.update!( + package_name_pattern: package_name_pattern, + package_type: package.package_type, + minimum_access_level_for_delete: minimum_access_level_for_delete + ) + end + + it_behaves_like params[:shared_examples_name] + end + + context 'for package with unsupported package type for package protection rule' do + let_it_be(:nuget_package) { create(:nuget_package, project: project) } + + let(:package) { nuget_package } + let(:package_file_id) { nuget_package.package_files.first.id } + + let(:headers) { headers_pat_project_maintainer } + + it_behaves_like 'deleting package' + end + end end end diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb index 674415f0b20..f84bf96c50c 100644 --- a/spec/requests/api/personal_access_tokens_spec.rb +++ b/spec/requests/api/personal_access_tokens_spec.rb @@ -180,6 +180,37 @@ RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category: end end + context 'filter with expires parameter' do + let_it_be(:token1) { create(:personal_access_token, expires_at: Date.new(2022, 01, 01)) } + + context 'test expires_before' do + where(:expires_at, :status, :result_count, :result) do + '2022-01-02' | :ok | 1 | lazy { [token1.id] } + '2022-01-01' | :ok | 0 | lazy { [] } + '2022-01-01T12:30:24' | :ok | 0 | lazy { [] } + 'asdf' | :bad_request | 1 | { "error" => "expires_before is invalid" } + end + + with_them do + it_behaves_like 'response as expected', expires_before: params[:expires_at] + end + end + + context 'test expires_after' do + where(:expires_at, :status, :result_count, :result) do + '2022-01-03' | :ok | 1 | lazy { [current_users_token.id] } + '2022-01-01' | :ok | 2 | lazy { [token1.id, current_users_token.id] } + '2022-01-01T12:30:26' | :ok | 2 | lazy { [token1.id, current_users_token.id] } + (DateTime.now + 1).to_s | :ok | 1 | lazy { [current_users_token.id] } + 'asdf' | :bad_request | 1 | { "error" => "expires_after is invalid" } + end + + with_them do + it_behaves_like 'response as expected', expires_after: params[:expires_at] + end + end + end + context 'filter with search parameter' do let_it_be(:token1) { create(:personal_access_token, name: 'test_1') } let_it_be(:token2) { create(:personal_access_token, name: 'test_2') } diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index d26bbdb7b06..927150b9f12 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -735,6 +735,94 @@ RSpec.describe API::ProjectPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:no_content) end end + + context 'with package protection rule for different roles and package_name_patterns', :enable_admin_mode do + let_it_be(:pat_project_maintainer) { create(:personal_access_token, user: create(:user, maintainer_of: [project])) } + let_it_be(:pat_project_owner) { create(:personal_access_token, user: create(:user, owner_of: [project])) } + let_it_be(:pat_instance_admin) { create(:personal_access_token, :admin_mode, user: create(:admin)) } + let_it_be(:headers_pat_project_maintainer) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_maintainer.token } } + let_it_be(:headers_pat_project_owner) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_project_owner.token } } + let_it_be(:headers_pat_instance_admin) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => pat_instance_admin.token } } + let_it_be(:job_from_project_maintainer) { create(:ci_build, :running, user: pat_project_maintainer.user, project: project) } + let_it_be(:job_from_project_owner) { create(:ci_build, :running, user: pat_project_owner.user, project: project) } + + let_it_be(:headers_job_token_from_maintainer) { { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_maintainer.token } } + let_it_be(:headers_job_token_from_owner) { { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_from_project_owner.token } } + + let(:package_protection_rule) { create(:package_protection_rule, project: project) } + + let(:package_name) { package1.name } + let(:package_name_no_match) { "#{package_name}_no_match" } + + subject do + delete api(package_url), headers: headers + response + end + + shared_examples 'deleting package protected' do + it_behaves_like 'returning response status', :forbidden + it do + subject + + expect(json_response).to include('message' => "403 Forbidden - Package is deletion protected.") + end + + it { expect { subject }.not_to change { ::Packages::Package.pending_destruction.count } } + + context 'when feature flag :packages_protected_packages_delete disabled' do + before do + stub_feature_flags(packages_protected_packages_delete: false) + end + + it_behaves_like 'deleting package' + end + end + + shared_examples 'deleting package' do + it_behaves_like 'returning response status', :no_content + it { expect { subject }.to change { ::Packages::Package.pending_destruction.count }.by(1) } + end + + where(:package_name_pattern, :minimum_access_level_for_delete, :headers, :shared_examples_name) do + ref(:package_name) | :owner | ref(:headers_job_token_from_maintainer) | 'deleting package protected' + ref(:package_name) | :owner | ref(:headers_job_token_from_owner) | 'deleting package' + ref(:package_name) | :owner | ref(:headers_pat_project_maintainer) | 'deleting package protected' + ref(:package_name) | :owner | ref(:headers_pat_project_owner) | 'deleting package' + ref(:package_name) | :owner | ref(:headers_pat_instance_admin) | 'deleting package' + + ref(:package_name) | :admin | ref(:headers_job_token_from_owner) | 'deleting package protected' + ref(:package_name) | :admin | ref(:headers_pat_project_maintainer) | 'deleting package protected' + ref(:package_name) | :admin | ref(:headers_pat_project_owner) | 'deleting package protected' + ref(:package_name) | :admin | ref(:headers_pat_instance_admin) | 'deleting package' + + ref(:package_name_no_match) | :owner | ref(:headers_pat_project_owner) | 'deleting package' + end + + with_them do + before do + package_protection_rule.update!( + package_name_pattern: package_name_pattern, + package_type: package1.package_type, + minimum_access_level_for_delete: minimum_access_level_for_delete + ) + end + + it_behaves_like params[:shared_examples_name] + end + + context 'for package with unsupported package type for package protection rule' do + let_it_be(:golang_package) { create(:golang_package, project: project, name: "#{project.root_namespace.path}/golang.org/x/pkg") } + + let(:headers) { headers_pat_project_maintainer } + + subject do + delete api("/projects/#{project.id}/packages/#{golang_package.id}"), headers: headers + response + end + + it_behaves_like 'deleting package' + end + end end context 'with a maven package' do diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb index dd0d5fe61c9..01f2237c3f0 100644 --- a/spec/requests/api/resource_access_tokens_spec.rb +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -4,7 +4,7 @@ require "spec_helper" RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do let_it_be(:user) { create(:user) } - let_it_be(:user_non_priviledged) { create(:user) } + let_it_be(:user_non_privileged) { create(:user) } shared_examples 'resource access token API' do |source_type| context "GET #{source_type}s/:id/access_tokens" do @@ -13,7 +13,11 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do context "when the user has valid permissions" do let_it_be(:project_bot) { create(:user, :project_bot, bot_namespace: namespace) } let_it_be(:active_access_tokens) { create_list(:personal_access_token, 5, user: project_bot) } - let_it_be(:expired_token) { create(:personal_access_token, :expired, user: project_bot) } + let_it_be(:expired_token) do + create(:personal_access_token, :expired, expires_at: 2.days.ago, last_used_at: 2.days.ago, name: 'a_test_1', + user: project_bot) + end + let_it_be(:revoked_token) { create(:personal_access_token, :revoked, user: project_bot) } let_it_be(:inactive_access_tokens) { [expired_token, revoked_token] } let_it_be(:all_access_tokens) { active_access_tokens + inactive_access_tokens } @@ -122,39 +126,128 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do end end - context 'when state param is set to inactive' do - let(:params) { { state: 'inactive' } } - - it 'returns only inactive tokens' do - get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params + context 'when filtering by revoked' do + it 'returns non-revoked tokens when revoked is false' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { revoked: false } token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array(all_access_tokens.pluck(:id).reject { |n| n == revoked_token.id }) + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('public_api/v4/resource_access_tokens') - expect(token_ids).to match_array(inactive_access_tokens.pluck(:id)) + it 'returns revoked tokens when revoked is true' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { revoked: true } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array([revoked_token.id]) end end - context 'when state param is set to active' do - let(:params) { { state: 'active' } } + context 'when filtering by state' do + context 'when state param is set to inactive' do + let(:params) { { state: 'inactive' } } - it 'returns only active tokens' do - get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params + it 'returns only inactive tokens' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params + + token_ids = json_response.map { |token| token['id'] } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/resource_access_tokens') + expect(token_ids).to match_array(inactive_access_tokens.pluck(:id)) + end + end + + context 'when state param is set to active' do + let(:params) { { state: 'active' } } + + it 'returns only active tokens' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: params + + token_ids = json_response.map { |token| token['id'] } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema('public_api/v4/resource_access_tokens') + expect(token_ids).to match_array(active_access_tokens.pluck(:id)) + end + end + end + + context 'when filtering by created dates' do + it 'returns tokens created before specified date' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { created_before: 1.day.ago } + + expect(json_response).to be_empty + end + + it 'returns tokens created after specified date' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { created_after: 1.day.ago } token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array(all_access_tokens.pluck(:id)) + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('public_api/v4/resource_access_tokens') - expect(token_ids).to match_array(active_access_tokens.pluck(:id)) + context 'when filtering by last used dates' do + it 'returns tokens last used before specified date' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { last_used_before: 1.day.ago } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array([expired_token.id]) + end + + it 'returns tokens last used after specified date' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { last_used_after: 1.day.ago } + + expect(json_response).to be_empty + end + end + + context 'when filtering by expiration dates' do + it 'returns tokens that expire before specified date' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { expires_before: 1.day.ago } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array([expired_token.id]) + end + + it 'returns tokens that expire after specified date' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { expires_after: 1.day.ago } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array(all_access_tokens.pluck(:id).reject { |n| n == expired_token.id }) + end + end + + context 'when searching by name' do + it 'returns tokens matching the search term' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { search: 'a_test_1' } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids).to match_array([expired_token.id]) + end + end + + context 'when sorting' do + it 'sorts tokens by last_used_desc when specified' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { sort: 'name_desc' } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids.last).to eq(expired_token.id) + end + + it 'sorts tokens by last_used_asc when specified' do + get api("/#{source_type}s/#{resource_id}/access_tokens", user), params: { sort: 'name_asc' } + + token_ids = json_response.map { |token| token['id'] } + expect(token_ids.first).to eq(expired_token.id) end end end context "when the user does not have valid permissions" do - let_it_be(:user) { user_non_priviledged } + let_it_be(:user) { user_non_privileged } let_it_be(:project_bot) { create(:user, :project_bot, bot_namespace: namespace) } let_it_be(:access_tokens) { create_list(:personal_access_token, 3, user: project_bot) } let_it_be(:resource_id) { resource.id } @@ -266,7 +359,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do end context "when the user does not have valid permissions" do - let_it_be(:user) { user_non_priviledged } + let_it_be(:user) { user_non_privileged } it "returns 401" do get_token @@ -340,7 +433,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do end context "when the user does not have valid permissions" do - let_it_be(:user) { user_non_priviledged } + let_it_be(:user) { user_non_privileged } it "does not delete the token, and returns 400", :aggregate_failures do delete_token @@ -483,7 +576,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do let_it_be(:resource_id) { resource.id } context "when the user role is too low" do - let_it_be(:user) { user_non_priviledged } + let_it_be(:user) { user_non_privileged } it "does not create the token, and returns the permission error" do create_token @@ -687,7 +780,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do before_all do resource.add_maintainer(user) other_resource.add_maintainer(user) - resource.add_developer(user_non_priviledged) + resource.add_developer(user_non_privileged) end it_behaves_like 'resource access token API', 'project' @@ -703,7 +796,7 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do before_all do resource.add_owner(user) other_resource.add_owner(user) - resource.add_maintainer(user_non_priviledged) + resource.add_maintainer(user_non_privileged) end it_behaves_like 'resource access token API', 'group' diff --git a/spec/services/packages/protection/check_delete_rule_existence_service_spec.rb b/spec/services/packages/protection/check_delete_rule_existence_service_spec.rb new file mode 100644 index 00000000000..bd4eb1fe1a5 --- /dev/null +++ b/spec/services/packages/protection/check_delete_rule_existence_service_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Protection::CheckDeleteRuleExistenceService, feature_category: :package_registry do + using RSpec::Parameterized::TableSyntax + + let_it_be(:project) { create(:project) } + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:project_developer) { create(:user, developer_of: project) } + let_it_be(:project_maintainer) { create(:user, maintainer_of: project) } + let_it_be(:project_owner) { project.owner } + let_it_be(:instance_admin) { create(:admin) } + + let_it_be(:container_protection_rule_npm) do + create(:package_protection_rule, + project: project, + package_type: :npm, + package_name_pattern: "@#{project.full_path}*", + minimum_access_level_for_delete: :owner) + end + + let_it_be(:container_protection_rule_pypi) do + create(:package_protection_rule, + project: project, + package_type: :pypi, + package_name_pattern: "#{project.full_path}*", + minimum_access_level_for_delete: :admin) + end + + let(:params) { { package_name: package_name, package_type: package_type } } + let(:service) { described_class.new(project: project, current_user: current_user, params: params) } + + subject(:service_response) { service.execute } + + shared_examples 'a service response for protection rule exists' do + it_behaves_like 'returning a success service response' + it { is_expected.to have_attributes(payload: { protection_rule_exists?: true }) } + end + + shared_examples 'a service response for protection rule does not exist' do + it_behaves_like 'returning a success service response' + it { is_expected.to have_attributes(payload: { protection_rule_exists?: false }) } + end + + shared_examples 'an error service response for unauthorized actor' do + it_behaves_like 'returning an error service response', message: 'Unauthorized' + it { is_expected.to have_attributes reason: :unauthorized } + end + + shared_examples 'an error service response for invalid package type' do + it_behaves_like 'returning an error service response', message: 'Invalid package type' + it { is_expected.to have_attributes reason: :invalid_package_type } + end + + describe '#execute', :enable_admin_mode do + # rubocop:disable Layout/LineLength -- Avoid formatting in favor of one-line table syntax + where(:package_name, :package_type, :current_user, :expected_shared_example) do + lazy { "@#{project.full_path}" } | :npm | ref(:project_developer) | 'an error service response for unauthorized actor' + lazy { "@#{project.full_path}" } | :npm | ref(:project_maintainer) | 'a service response for protection rule exists' + lazy { "@#{project.full_path}" } | :npm | ref(:project_owner) | 'a service response for protection rule does not exist' + lazy { "@#{project.full_path}" } | :npm | ref(:instance_admin) | 'a service response for protection rule does not exist' + lazy { "@other-scope/#{project.full_path}" } | :npm | ref(:project_maintainer) | 'a service response for protection rule does not exist' + lazy { "@other-scope/#{project.full_path}" } | :npm | ref(:project_owner) | 'a service response for protection rule does not exist' + lazy { project.full_path } | :pypi | ref(:project_maintainer) | 'a service response for protection rule exists' + lazy { project.full_path } | :pypi | ref(:project_owner) | 'a service response for protection rule exists' + lazy { project.full_path } | :pypi | ref(:instance_admin) | 'a service response for protection rule does not exist' + + # Edge cases + lazy { "@#{project.full_path}" } | :npm | ref(:unauthorized_user) | 'an error service response for unauthorized actor' + lazy { "@#{project.full_path}" } | :npm | nil | 'an error service response for unauthorized actor' + lazy { "@#{project.full_path}" } | :no_type | nil | 'an error service response for invalid package type' + lazy { "@#{project.full_path}" } | nil | ref(:project_owner) | 'an error service response for invalid package type' + nil | :npm | ref(:project_owner) | 'a service response for protection rule does not exist' + nil | nil | ref(:project_owner) | 'an error service response for invalid package type' + end + # rubocop:enable Layout/LineLength + + with_them do + it_behaves_like params[:expected_shared_example] + end + end +end diff --git a/spec/support/shared_examples/lib/api/access_token_shared_examples.rb b/spec/support/shared_examples/lib/api/access_token_shared_examples.rb index daa389ee259..801eaa70dc9 100644 --- a/spec/support/shared_examples/lib/api/access_token_shared_examples.rb +++ b/spec/support/shared_examples/lib/api/access_token_shared_examples.rb @@ -43,6 +43,20 @@ RSpec.shared_examples 'an access token GET API with access token params' do ) end + context 'when filtering by revoked' do + it 'returns not-revoked tokens when revoked is false' do + get api_request, params: { revoked: false } + + expect_paginated_array_response_contain_exactly(*all_token_ids.excluding(revoked_token1.id, revoked_token2.id)) + end + + it 'returns revoked tokens when revoked is true' do + get api_request, params: { revoked: true } + + expect_paginated_array_response_contain_exactly(revoked_token1.id, revoked_token2.id) + end + end + context 'when filtering by state' do it 'returns active tokens when state is active' do get api_request, params: { state: 'active' } @@ -68,20 +82,6 @@ RSpec.shared_examples 'an access token GET API with access token params' do end end - context 'when filtering by revoked' do - it 'returns not-revoked tokens when revoked is false' do - get api_request, params: { revoked: false } - - expect_paginated_array_response_contain_exactly(*all_token_ids.excluding(revoked_token1.id, revoked_token2.id)) - end - - it 'returns revoked tokens when revoked is true' do - get api_request, params: { revoked: true } - - expect_paginated_array_response_contain_exactly(revoked_token1.id, revoked_token2.id) - end - end - context 'when filtering by created dates' do it 'returns tokens created before specified date' do get api_request, params: { created_before: 1.day.ago } @@ -110,6 +110,20 @@ RSpec.shared_examples 'an access token GET API with access token params' do end end + context 'when filtering by expiration dates' do + it 'returns tokens that expire before specified date' do + get api_request, params: { expires_before: 1.year.ago + 1.day } + + expect_paginated_array_response_contain_exactly(expired_token1.id, expired_token2.id) + end + + it 'returns tokens that expire after specified date' do + get api_request, params: { expires_after: 1.year.ago, expires_before: 1.week.ago } + + expect_paginated_array_response_contain_exactly(expired_token1.id, expired_token2.id) + end + end + context 'when searching by name' do it 'returns tokens matching the search term' do get api_request, params: { search: 'test' } diff --git a/yarn.lock b/yarn.lock index 31858310987..0da735c6ffb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1441,10 +1441,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.123.0.tgz#1fa3b1a709755ff7c8ef67e18c0442101655ebf0" integrity sha512-yjVn+utOTIKk8d9JlvGo6EgJ4TQ+CKpe3RddflAqtsQqQuL/2MlVdtaUePybxYzWIaumFuh5LouQ6BrWyw1niQ== -"@gitlab/ui@110.0.0": - version "110.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-110.0.0.tgz#3a1fd77513063d338b8a4676453e53321f58230b" - integrity sha512-AYiPQTQW3Hh+3j0eZr1WvuzDVNHEssxgrO66jbQO79WmSr88IXO13kICAbs0T1bvmS2vZx8VyP/TQ0ow3UJMHQ== +"@gitlab/ui@110.1.0": + version "110.1.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-110.1.0.tgz#5a38aafb92d67b589318c39f72b5ba622fa89b83" + integrity sha512-tCezdqWgSNKuksfvVfm8TWBSIbkuK0jhCoffFFKl3HzBf9FWnCqS5+XEHLU3nPttZBTi5T761BTNqqHx8SZUAg== dependencies: "@floating-ui/dom" "1.4.3" echarts "^5.3.2"