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.
+
+
+
+### After
+
+The information is easier to scan if you move it into concepts and [tasks](task.md).
+
+#### Concept
+
+
+
+#### Task
+
+
diff --git a/doc/development/documentation/topic_types/img/example_1.png b/doc/development/documentation/topic_types/img/example_1.png
new file mode 100644
index 00000000000..b12d6df44b9
Binary files /dev/null and b/doc/development/documentation/topic_types/img/example_1.png differ
diff --git a/doc/development/documentation/topic_types/img/example_1_after_concept.png b/doc/development/documentation/topic_types/img/example_1_after_concept.png
new file mode 100644
index 00000000000..fe1b8dfe31f
Binary files /dev/null and b/doc/development/documentation/topic_types/img/example_1_after_concept.png differ
diff --git a/doc/development/documentation/topic_types/img/example_1_after_task.png b/doc/development/documentation/topic_types/img/example_1_after_task.png
new file mode 100644
index 00000000000..ede5de62dd8
Binary files /dev/null and b/doc/development/documentation/topic_types/img/example_1_after_task.png differ
diff --git a/doc/development/documentation/topic_types/img/reference_example1.png b/doc/development/documentation/topic_types/img/reference_example1.png
new file mode 100644
index 00000000000..4b421c62cd0
Binary files /dev/null and b/doc/development/documentation/topic_types/img/reference_example1.png differ
diff --git a/doc/development/documentation/topic_types/img/reference_example2.png b/doc/development/documentation/topic_types/img/reference_example2.png
new file mode 100644
index 00000000000..e026412e2e4
Binary files /dev/null and b/doc/development/documentation/topic_types/img/reference_example2.png differ
diff --git a/doc/development/documentation/topic_types/reference.md b/doc/development/documentation/topic_types/reference.md
index fcc5ef649a9..83200a1addc 100644
--- a/doc/development/documentation/topic_types/reference.md
+++ b/doc/development/documentation/topic_types/reference.md
@@ -34,3 +34,18 @@ Avoid these topic titles:
for a task, or information about a concept.
- `Limitations`. Instead, move the content near other similar information.
If you must, you can use the title `Known issues`.
+
+## Example
+
+### Before
+
+This topic was a compilation of a variety of information and was difficult to scan.
+
+
+
+### 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.
+
+
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"