From 08d259cc2b630adf3aba5927bca070521cc2b9a1 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 11 Mar 2025 18:12:11 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile | 2 +- Gemfile.checksum | 2 +- Gemfile.lock | 4 +- Gemfile.next.checksum | 2 +- Gemfile.next.lock | 4 +- .../javascripts/ci/job_details/job_app.vue | 1 + app/assets/javascripts/issuable/index.js | 35 - .../javascripts/merge_requests/list/index.js | 34 - .../components/milestone_combobox.vue | 8 +- .../javascripts/milestones/stores/index.js | 17 +- .../components/model_experiments_header.vue | 10 +- .../promote/model_selection_dropdown.vue | 4 +- .../routes/candidates/promote/promote_run.vue | 36 +- .../candidates/show/candidate_detail.vue | 28 +- .../index/components/ml_experiments_index.vue | 2 +- .../pages/groups/merge_requests/index.js | 19 - .../projects/merge_requests/index/index.js | 20 +- .../components/pipeline_status_chart.vue | 2 +- app/assets/javascripts/releases/mount_edit.js | 2 + app/assets/javascripts/releases/mount_new.js | 2 + app/controllers/groups_controller.rb | 6 +- .../projects/merge_requests_controller.rb | 2 +- .../active_sessions_controller.rb | 2 +- app/models/packages/protection/rule.rb | 5 + app/models/user.rb | 13 + .../check_delete_rule_existence_service.rb | 44 + .../check_rule_existence_service.rb | 3 +- .../user_detail_onboarding_status.json | 3 +- app/views/groups/merge_requests.html.haml | 27 +- .../merge_requests/_nav_btns.html.haml | 10 - .../projects/merge_requests/index.html.haml | 31 +- .../issuable/_bulk_update_sidebar.html.haml | 9 +- app/views/shared/projects/_project.html.haml | 8 +- .../beta/vue_merge_request_list.yml | 9 - .../packages_protected_packages_delete.yml | 9 + config/initializers/8_devise.rb | 6 +- config/initializers/validate_cell_config.rb | 17 +- ...306065243_remove_broken_fk_a2141b1522_p.rb | 38 +- db/structure.sql | 3 + doc/api/graphql/reference/_index.md | 1 + doc/api/packages.md | 11 +- doc/ci/jobs/job_artifacts.md | 2 + doc/ci/runners/configure_runners.md | 38 +- .../documentation/topic_types/_index.md | 4 + .../documentation/topic_types/concept.md | 21 + .../topic_types/img/example_1.png | Bin 0 -> 55655 bytes .../img/example_1_after_concept.png | Bin 0 -> 17218 bytes .../topic_types/img/example_1_after_task.png | Bin 0 -> 11257 bytes .../topic_types/img/reference_example1.png | Bin 0 -> 23177 bytes .../topic_types/img/reference_example2.png | Bin 0 -> 23115 bytes .../documentation/topic_types/reference.md | 15 + .../documentation/topic_types/task.md | 1 + doc/development/sql.md | 37 +- .../configuration/customize_settings.md | 75 + .../vulnerability_report/_index.md | 2 +- doc/user/gitlab_duo/choose_path.md | 169 +++ .../helpers/personal_access_tokens_helpers.rb | 4 + lib/api/package_files.rb | 11 + lib/api/project_packages.rb | 11 + lib/api/resource_access_tokens.rb | 7 +- lib/gitlab/internal_events.rb | 2 + locale/gitlab.pot | 153 +- package.json | 2 +- scripts/frontend/quarantined_vue3_specs.txt | 1 - spec/controllers/groups_controller_spec.rb | 86 -- .../merge_requests_controller_spec.rb | 71 - ..._users_dropdowns_in_issuables_list_spec.rb | 19 - .../filtered_search/dropdown_user_spec.js | 2 +- .../filtered_search/dropdown_utils_spec.js | 2 +- spec/frontend/fixtures/merge_requests.rb | 13 - .../fixtures/static/merge_request_list.html | 1260 +++++++++++++++++ .../components/pipeline_status_chart_spec.js | 4 +- .../releases/components/app_edit_new_spec.js | 25 + .../initializers/validate_cell_config_spec.rb | 24 +- spec/lib/gitlab/internal_events_spec.rb | 14 + spec/models/user_spec.rb | 4 +- spec/requests/api/package_files_spec.rb | 109 +- .../api/personal_access_tokens_spec.rb | 31 + spec/requests/api/project_packages_spec.rb | 88 ++ .../api/resource_access_tokens_spec.rb | 143 +- ...heck_delete_rule_existence_service_spec.rb | 83 ++ .../lib/api/access_token_shared_examples.rb | 42 +- yarn.lock | 8 +- 83 files changed, 2474 insertions(+), 600 deletions(-) create mode 100644 app/services/packages/protection/check_delete_rule_existence_service.rb delete mode 100644 app/views/projects/merge_requests/_nav_btns.html.haml delete mode 100644 config/feature_flags/beta/vue_merge_request_list.yml create mode 100644 config/feature_flags/gitlab_com_derisk/packages_protected_packages_delete.yml create mode 100644 doc/development/documentation/topic_types/img/example_1.png create mode 100644 doc/development/documentation/topic_types/img/example_1_after_concept.png create mode 100644 doc/development/documentation/topic_types/img/example_1_after_task.png create mode 100644 doc/development/documentation/topic_types/img/reference_example1.png create mode 100644 doc/development/documentation/topic_types/img/reference_example2.png create mode 100644 doc/user/gitlab_duo/choose_path.md create mode 100644 spec/frontend/fixtures/static/merge_request_list.html create mode 100644 spec/services/packages/protection/check_delete_rule_existence_service_spec.rb 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 0000000000000000000000000000000000000000..b12d6df44b904b283c1246630ba258f71089e627 GIT binary patch literal 55655 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe1b8dfe31f1458ce4a3fd399caa4e78230fede0 GIT binary patch literal 17218 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

UL)A5dW(?vjq)Mz%4LVj$UXN`kL-e;*)n& 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 E3tm-HRR910 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ede5de62dd8e32f70dd3076efacfec4647d11282 GIT binary patch literal 11257 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 NGM^MAtHcch{{v+qAt3+& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4b421c62cd0ad64d9f54ba57fb33e73b4da5c162 GIT binary patch literal 23177 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e026412e2e421711beeb4746bbeb9757d7992912 GIT binary patch literal 23115 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 literal 0 HcmV?d00001 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"