diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs
index 3bb95b243ec..5922a318033 100644
--- a/.eslint_todo/vue-no-unused-properties.mjs
+++ b/.eslint_todo/vue-no-unused-properties.mjs
@@ -593,9 +593,6 @@ export default {
'ee/app/assets/javascripts/tracing/details/tracing_details.vue',
'ee/app/assets/javascripts/usage_quotas/code_suggestions/components/code_suggestions_info_card.vue',
'ee/app/assets/javascripts/usage_quotas/code_suggestions/components/search_and_sort_bar.vue',
- 'ee/app/assets/javascripts/usage_quotas/pipelines/components/app.vue',
- 'ee/app/assets/javascripts/usage_quotas/pipelines/components/minutes_usage_per_project_chart.vue',
- 'ee/app/assets/javascripts/usage_quotas/pipelines/components/shared_runner_usage_month_chart.vue',
'ee/app/assets/javascripts/usage_quotas/seats/components/statistics_seats_card.vue',
'ee/app/assets/javascripts/usage_quotas/transfer/components/usage_by_month.vue',
'ee/app/assets/javascripts/users/identity_verification/components/credit_card_verification.vue',
diff --git a/.rubocop_todo/rspec/be_nil.yml b/.rubocop_todo/rspec/be_nil.yml
index 2ccf04bd4e2..c0ce8404f75 100644
--- a/.rubocop_todo/rspec/be_nil.yml
+++ b/.rubocop_todo/rspec/be_nil.yml
@@ -2,27 +2,6 @@
# Cop supports --autocorrect.
RSpec/BeNil:
Exclude:
- - 'ee/spec/services/app_sec/dast/profiles/update_service_spec.rb'
- - 'ee/spec/services/gitlab_subscriptions/reconciliations/calculate_seat_count_data_service_spec.rb'
- - 'ee/spec/workers/concerns/geo/skip_secondary_spec.rb'
- - 'ee/spec/workers/repository_update_mirror_worker_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/10_govern/scan_execution_policy_vulnerabilities_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/10_govern/scan_result_policy_license_finding_spec.rb'
- - 'qa/qa/specs/features/ee/browser_ui/10_govern/scan_result_policy_vulnerabilities_spec.rb'
- - 'qa/spec/page/element_spec.rb'
- - 'qa/spec/service/docker_run/mixins/third_party_docker_spec.rb'
- - 'qa/spec/service/shellout_spec.rb'
- - 'spec/config/object_store_settings_spec.rb'
- - 'spec/controllers/application_controller_spec.rb'
- - 'spec/dot_gitlab_ci/ci_configuration_validation/shared_context_and_examples.rb'
- - 'spec/features/admin/users/admin_impersonates_user_spec.rb'
- - 'spec/finders/container_repositories_finder_spec.rb'
- - 'spec/finders/uploader_finder_spec.rb'
- - 'spec/graphql/mutations/issues/set_due_date_spec.rb'
- - 'spec/graphql/resolvers/container_repositories_resolver_spec.rb'
- - 'spec/graphql/resolvers/paginated_tree_resolver_spec.rb'
- - 'spec/graphql/resolvers/tree_resolver_spec.rb'
- - 'spec/graphql/resolvers/users/group_count_resolver_spec.rb'
- 'spec/helpers/namespaces_helper_spec.rb'
- 'spec/helpers/tree_helper_spec.rb'
- 'spec/helpers/version_check_helper_spec.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5d07e610b98..7d20b85656a 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-6af2d5f99e37feee2b7221af5f276040b8109195
+7dbf8d6fbb832f81e2bfb0a4c143a0932cbecf53
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 4b83a03173d..52622c6e8c9 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-93b9e36e23c2a4e51dc2012932830b72c8f838aa
+fecc9b9bcb1ab8b69fb72be11705fd47925302d2
diff --git a/app/assets/javascripts/credentials/components/credentials_filter_app.vue b/app/assets/javascripts/credentials/components/credentials_filter_sort_app.vue
similarity index 53%
rename from app/assets/javascripts/credentials/components/credentials_filter_app.vue
rename to app/assets/javascripts/credentials/components/credentials_filter_sort_app.vue
index b3c9dae7ff9..27b6fb123fa 100644
--- a/app/assets/javascripts/credentials/components/credentials_filter_app.vue
+++ b/app/assets/javascripts/credentials/components/credentials_filter_sort_app.vue
@@ -1,38 +1,43 @@
-
+
+
+
+
diff --git a/app/assets/javascripts/credentials/constants.js b/app/assets/javascripts/credentials/constants.js
index 883fab05753..854b1f7a21f 100644
--- a/app/assets/javascripts/credentials/constants.js
+++ b/app/assets/javascripts/credentials/constants.js
@@ -6,6 +6,10 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import DateToken from '~/vue_shared/components/filtered_search_bar/tokens/date_token.vue';
+export const SORT_KEY_NAME = 'name';
+export const SORT_KEY_CREATED = 'created';
+export const SORT_KEY_EXPIRES = 'expires';
+
export const TOKENS = [
{
icon: 'key',
@@ -73,3 +77,34 @@ export const TOKENS = [
unique: true,
},
];
+
+export const SORT_OPTIONS = [
+ {
+ text: __('Name'),
+ value: SORT_KEY_NAME,
+ sort: {
+ asc: 'name_asc',
+ desc: 'name_desc',
+ },
+ },
+ {
+ text: __('Created date'),
+ value: SORT_KEY_CREATED,
+ sort: {
+ asc: 'created_asc',
+ desc: 'created_desc',
+ },
+ },
+ {
+ text: __('Expiration date'),
+ value: SORT_KEY_EXPIRES,
+ sort: {
+ asc: 'expires_at_asc_id_desc',
+ },
+ },
+];
+
+export const DEFAULT_SORT = {
+ value: SORT_KEY_EXPIRES,
+ isAsc: true,
+};
diff --git a/app/assets/javascripts/credentials/index.js b/app/assets/javascripts/credentials/index.js
index 614a02b7a0b..abf054077a9 100644
--- a/app/assets/javascripts/credentials/index.js
+++ b/app/assets/javascripts/credentials/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import CredentialsFilterApp from './components/credentials_filter_app.vue';
+import CredentialsFilterSortApp from './components/credentials_filter_sort_app.vue';
-export const initCredentialsFilterApp = () => {
+export const initCredentialsFilterSortApp = () => {
return new Vue({
- el: document.querySelector('#js-credentials-filter-app'),
- render: (createElement) => createElement(CredentialsFilterApp),
+ el: document.querySelector('#js-credentials-filter-sort-app'),
+ render: (createElement) => createElement(CredentialsFilterSortApp),
});
};
diff --git a/app/assets/javascripts/credentials/utils.js b/app/assets/javascripts/credentials/utils.js
index c54db68f5c7..af646ca6dde 100644
--- a/app/assets/javascripts/credentials/utils.js
+++ b/app/assets/javascripts/credentials/utils.js
@@ -1,9 +1,9 @@
-import { queryToObject } from '~/lib/utils/url_utility';
+import { queryToObject, setUrlParams } from '~/lib/utils/url_utility';
import {
OPERATORS_BEFORE,
OPERATORS_AFTER,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import { TOKENS } from './constants';
+import { TOKENS, SORT_OPTIONS, DEFAULT_SORT } from './constants';
/**
* @typedef {{type: string, value: {data: string, operator: string}}} Token
@@ -16,9 +16,10 @@ import { TOKENS } from './constants';
* @returns {Array}
*/
export function initializeValuesFromQuery(query = document.location.search) {
- const tokens = [];
+ const tokens = /** @type {Array} */ ([]);
+ const sorting = DEFAULT_SORT;
- const { search, ...terms } = queryToObject(query);
+ const { search, sort, ...terms } = queryToObject(query);
for (const [key, value] of Object.entries(terms)) {
const isBefore = key.endsWith('_before');
@@ -54,5 +55,18 @@ export function initializeValuesFromQuery(query = document.location.search) {
tokens.push(search);
}
- return tokens;
+ const sortOption = SORT_OPTIONS.find((item) => [item.sort.desc, item.sort.asc].includes(sort));
+ if (sort && sortOption) {
+ sorting.value = sortOption.value;
+ sorting.isAsc = sortOption.sort.asc === sort;
+ }
+
+ return { tokens, sorting };
+}
+
+export function buildSortedUrl(value, isAsc) {
+ const sortedOption = SORT_OPTIONS.find((sortOption) => sortOption.value === value);
+ const sort = isAsc ? sortedOption.sort.asc : sortedOption.sort.desc;
+ const newUrl = setUrlParams({ sort });
+ return newUrl;
}
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index c6aadaee3ef..85756c4e486 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -639,15 +639,12 @@ export default {
Mousetrap.bind(keysFor(MR_PREVIOUS_FILE_IN_DIFF), () => this.jumpToFile(-1));
Mousetrap.bind(keysFor(MR_NEXT_FILE_IN_DIFF), () => this.jumpToFile(+1));
-
- if (this.commit) {
- Mousetrap.bind(keysFor(MR_COMMITS_NEXT_COMMIT), () =>
- this.moveToNeighboringCommit({ direction: 'next' }),
- );
- Mousetrap.bind(keysFor(MR_COMMITS_PREVIOUS_COMMIT), () =>
- this.moveToNeighboringCommit({ direction: 'previous' }),
- );
- }
+ Mousetrap.bind(keysFor(MR_COMMITS_NEXT_COMMIT), () =>
+ this.moveToNeighboringCommit({ direction: 'next' }),
+ );
+ Mousetrap.bind(keysFor(MR_COMMITS_PREVIOUS_COMMIT), () =>
+ this.moveToNeighboringCommit({ direction: 'previous' }),
+ );
Mousetrap.bind(['mod+f', 'mod+g'], () => {
this.keydownTime = new Date().getTime();
diff --git a/app/assets/javascripts/pages/admin/credentials/index.js b/app/assets/javascripts/pages/admin/credentials/index.js
index 46437aaed5b..10e2411aaff 100644
--- a/app/assets/javascripts/pages/admin/credentials/index.js
+++ b/app/assets/javascripts/pages/admin/credentials/index.js
@@ -1,5 +1,5 @@
import initConfirmModal from '~/confirm_modal';
-import { initCredentialsFilterApp } from '~/credentials';
+import { initCredentialsFilterSortApp } from '~/credentials';
initConfirmModal();
-initCredentialsFilterApp();
+initCredentialsFilterSortApp();
diff --git a/app/assets/javascripts/pages/groups/security/credentials/index.js b/app/assets/javascripts/pages/groups/security/credentials/index.js
index 46437aaed5b..10e2411aaff 100644
--- a/app/assets/javascripts/pages/groups/security/credentials/index.js
+++ b/app/assets/javascripts/pages/groups/security/credentials/index.js
@@ -1,5 +1,5 @@
import initConfirmModal from '~/confirm_modal';
-import { initCredentialsFilterApp } from '~/credentials';
+import { initCredentialsFilterSortApp } from '~/credentials';
initConfirmModal();
-initCredentialsFilterApp();
+initCredentialsFilterSortApp();
diff --git a/app/assets/javascripts/rapid_diffs/app/file_browser.vue b/app/assets/javascripts/rapid_diffs/app/file_browser.vue
new file mode 100644
index 00000000000..0ebcb016fb4
--- /dev/null
+++ b/app/assets/javascripts/rapid_diffs/app/file_browser.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/rapid_diffs/app/index.js b/app/assets/javascripts/rapid_diffs/app/index.js
index fa6605cd8ce..81a2616b4c0 100644
--- a/app/assets/javascripts/rapid_diffs/app/index.js
+++ b/app/assets/javascripts/rapid_diffs/app/index.js
@@ -3,7 +3,7 @@ import { initViewSettings } from '~/rapid_diffs/app/view_settings';
import { DiffFile } from '~/rapid_diffs/diff_file';
import { DiffFileMounted } from '~/rapid_diffs/diff_file_mounted';
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
-import { initFileBrowser } from '~/rapid_diffs/app/file_browser';
+import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser';
// This facade interface joins together all the bits and pieces of Rapid Diffs: DiffFile, Settings, File browser, etc.
// It's a unified entrypoint for Rapid Diffs and all external communications should happen through this interface.
diff --git a/app/assets/javascripts/rapid_diffs/app/file_browser.js b/app/assets/javascripts/rapid_diffs/app/init_file_browser.js
similarity index 52%
rename from app/assets/javascripts/rapid_diffs/app/file_browser.js
rename to app/assets/javascripts/rapid_diffs/app/init_file_browser.js
index ff60a731df1..347d5c1bcc5 100644
--- a/app/assets/javascripts/rapid_diffs/app/file_browser.js
+++ b/app/assets/javascripts/rapid_diffs/app/init_file_browser.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
import store from '~/mr_notes/stores';
-import DiffFileTree from '~/diffs/components/diffs_file_tree.vue';
+import { pinia } from '~/pinia/instance';
+import { DiffFile } from '~/rapid_diffs/diff_file';
+import FileBrowser from './file_browser.vue';
export async function initFileBrowser() {
const el = document.querySelector('[data-file-browser]');
@@ -9,21 +11,21 @@ export async function initFileBrowser() {
store.state.diffs.endpointMetadata = metadataEndpoint;
await store.dispatch('diffs/fetchDiffFilesMeta');
+ const loadedFiles = Object.fromEntries(DiffFile.getAll().map((file) => [file.id, true]));
+
// eslint-disable-next-line no-new
new Vue({
el,
- data() {
- return {
- visible: true,
- };
- },
store,
+ pinia,
render(h) {
- return h(DiffFileTree, {
- props: { visible: this.visible },
+ return h(FileBrowser, {
+ props: {
+ loadedFiles,
+ },
on: {
- toggled: () => {
- this.visible = !this.visible;
+ clickFile(file) {
+ DiffFile.findByFileHash(file.fileHash).selectFile();
},
},
});
diff --git a/app/assets/javascripts/rapid_diffs/diff_file.js b/app/assets/javascripts/rapid_diffs/diff_file.js
index 0b4f773d3d4..448b623095d 100644
--- a/app/assets/javascripts/rapid_diffs/diff_file.js
+++ b/app/assets/javascripts/rapid_diffs/diff_file.js
@@ -1,3 +1,4 @@
+import { DIFF_FILE_MOUNTED } from './dom_events';
import { VIEWER_ADAPTERS } from './adapters';
// required for easier mocking in tests
import IntersectionObserver from './intersection_observer';
@@ -24,11 +25,11 @@ export class DiffFile extends HTMLElement {
adapterConfig = VIEWER_ADAPTERS;
static findByFileHash(hash) {
- return document.querySelector(`diff-file#${hash}`);
+ return document.querySelector(`diff-file[id="${hash}"]`);
}
static getAll() {
- return document.querySelectorAll('diff-file');
+ return Array.from(document.querySelectorAll('diff-file'));
}
mount() {
@@ -38,6 +39,7 @@ export class DiffFile extends HTMLElement {
this.observeVisibility();
this.diffElement.addEventListener('click', this.onClick.bind(this));
this.trigger(events.MOUNTED);
+ this.dispatchEvent(new CustomEvent(DIFF_FILE_MOUNTED, { bubbles: true }));
}
trigger(event, ...args) {
@@ -73,6 +75,11 @@ export class DiffFile extends HTMLElement {
this.trigger(events.CLICK, event);
}
+ selectFile() {
+ this.scrollIntoView();
+ // TODO: add outline for active file
+ }
+
get data() {
const data = { ...this.dataset };
// viewer is dynamic, should be accessed via this.viewer
diff --git a/app/assets/javascripts/rapid_diffs/dom_events.js b/app/assets/javascripts/rapid_diffs/dom_events.js
new file mode 100644
index 00000000000..59d879b6f9d
--- /dev/null
+++ b/app/assets/javascripts/rapid_diffs/dom_events.js
@@ -0,0 +1 @@
+export const DIFF_FILE_MOUNTED = 'DiffFileMounted';
diff --git a/app/assets/stylesheets/components/rapid_diffs/diff_file_component.scss b/app/assets/stylesheets/components/rapid_diffs/diff_file_component.scss
index 457554bdd74..0991aeb2cd3 100644
--- a/app/assets/stylesheets/components/rapid_diffs/diff_file_component.scss
+++ b/app/assets/stylesheets/components/rapid_diffs/diff_file_component.scss
@@ -1,3 +1,10 @@
+@import 'framework/variables';
+
+.rd-diff-file-component {
+ // TODO: this must be defined using CSS Custom Properties to work across all pages
+ scroll-margin-top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{12px});
+}
+
.rd-diff-file {
padding-bottom: $gl-padding;
diff --git a/app/components/rapid_diffs/diff_file_component.html.haml b/app/components/rapid_diffs/diff_file_component.html.haml
index 948278b97c2..63ca9715a91 100644
--- a/app/components/rapid_diffs/diff_file_component.html.haml
+++ b/app/components/rapid_diffs/diff_file_component.html.haml
@@ -1,6 +1,6 @@
-# TODO: add fork suggestion (commits only)
-%diff-file{ id: id, data: server_data }
+%diff-file.rd-diff-file-component{ id: id, data: server_data }
.rd-diff-file
= render RapidDiffs::DiffFileHeaderComponent.new(diff_file: @diff_file)
-# extra wrapper needed so content-visibility: hidden doesn't require removing border or other styles
diff --git a/app/components/rapid_diffs/diff_file_component.rb b/app/components/rapid_diffs/diff_file_component.rb
index 1bc5246fde4..3fa068af61f 100644
--- a/app/components/rapid_diffs/diff_file_component.rb
+++ b/app/components/rapid_diffs/diff_file_component.rb
@@ -10,7 +10,7 @@ module RapidDiffs
end
def id
- @diff_file.file_identifier_hash
+ @diff_file.file_hash
end
def server_data
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 0616cd4d476..cdcdd403b24 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -21,7 +21,7 @@ class ProcessCommitWorker
loggable_arguments 2, 3
deduplicate :until_executed, feature_flag: :deduplicate_process_commit_worker
- concurrency_limit -> { 1000 if Feature.enabled?(:concurrency_limit_process_commit_worker, Feature.current_request) }
+ concurrency_limit -> { 1000 }
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
diff --git a/config/feature_flags/worker/concurrency_limit_process_commit_worker.yml b/config/feature_flags/worker/concurrency_limit_process_commit_worker.yml
deleted file mode 100644
index d4bc513708e..00000000000
--- a/config/feature_flags/worker/concurrency_limit_process_commit_worker.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: concurrency_limit_process_commit_worker
-feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472602
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171786
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/502784
-milestone: '17.6'
-group: group::source code
-type: worker
-default_enabled: false
diff --git a/db/docs/batched_background_migrations/backfill_protected_environment_approval_rules_protected_environment_project_id.yml b/db/docs/batched_background_migrations/backfill_protected_environment_approval_rules_protected_environment_project_id.yml
index 705279670ea..484a4011f86 100644
--- a/db/docs/batched_background_migrations/backfill_protected_environment_approval_rules_protected_environment_project_id.yml
+++ b/db/docs/batched_background_migrations/backfill_protected_environment_approval_rules_protected_environment_project_id.yml
@@ -1,8 +1,9 @@
---
migration_job_name: BackfillProtectedEnvironmentApprovalRulesProtectedEnvironmentProjectId
-description: Backfills sharding key `protected_environment_approval_rules.protected_environment_project_id` from `protected_environments`.
+description: Backfills sharding key `protected_environment_approval_rules.protected_environment_project_id`
+ from `protected_environments`.
feature_category: continuous_delivery
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162704
milestone: '17.3'
queued_migration_version: 20240814104154
-finalized_by: # version of the migration that finalized this BBM
+finalized_by: '20250220231747'
diff --git a/db/docs/draft_notes.yml b/db/docs/draft_notes.yml
index d37a8192b88..1fe47721d2d 100644
--- a/db/docs/draft_notes.yml
+++ b/db/docs/draft_notes.yml
@@ -8,14 +8,6 @@ description: Notes created during the review of an MR that are not yet published
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4213
milestone: '11.4'
gitlab_schema: gitlab_main_cell
-desired_sharding_key:
- project_id:
- references: projects
- backfill_via:
- parent:
- foreign_key: merge_request_id
- table: merge_requests
- sharding_key: target_project_id
- belongs_to: merge_request
-desired_sharding_key_migration_job_name: BackfillDraftNotesProjectId
table_size: small
+sharding_key:
+ project_id: projects
diff --git a/db/migrate/20250212110138_add_organization_id_to_ai_duo_chat_events.rb b/db/migrate/20250212110138_add_organization_id_to_ai_duo_chat_events.rb
new file mode 100644
index 00000000000..d35d7da929c
--- /dev/null
+++ b/db/migrate/20250212110138_add_organization_id_to_ai_duo_chat_events.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddOrganizationIdToAiDuoChatEvents < Gitlab::Database::Migration[2.2]
+ milestone '17.10'
+
+ def change
+ add_column :ai_duo_chat_events, :organization_id, :bigint
+ end
+end
diff --git a/db/post_migrate/20250210065034_validate_draft_notes_project_id_not_null_constraint.rb b/db/post_migrate/20250210065034_validate_draft_notes_project_id_not_null_constraint.rb
new file mode 100644
index 00000000000..5799aeecd3d
--- /dev/null
+++ b/db/post_migrate/20250210065034_validate_draft_notes_project_id_not_null_constraint.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ValidateDraftNotesProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
+ milestone '17.10'
+
+ def up
+ validate_not_null_constraint :draft_notes, :project_id
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20250212132647_add_ai_duo_chat_events_organization_id_index.rb b/db/post_migrate/20250212132647_add_ai_duo_chat_events_organization_id_index.rb
new file mode 100644
index 00000000000..59471397232
--- /dev/null
+++ b/db/post_migrate/20250212132647_add_ai_duo_chat_events_organization_id_index.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddAiDuoChatEventsOrganizationIdIndex < Gitlab::Database::Migration[2.2]
+ include Gitlab::Database::PartitioningMigrationHelpers
+
+ disable_ddl_transaction!
+ milestone '17.10'
+
+ INDEX_NAME = 'index_ai_duo_chat_events_on_organization_id'
+
+ def up
+ add_concurrent_partitioned_index :ai_duo_chat_events, :organization_id, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_partitioned_index_by_name :ai_duo_chat_events, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20250213125548_add_noteable_id_noteable_type_and_id_index_in_notes_table.rb b/db/post_migrate/20250213125548_add_noteable_id_noteable_type_and_id_index_in_notes_table.rb
new file mode 100644
index 00000000000..a049fe42371
--- /dev/null
+++ b/db/post_migrate/20250213125548_add_noteable_id_noteable_type_and_id_index_in_notes_table.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# This index was prepared in 17.9 PrepareNoteableIdNoteableTypeAndIdIndexInNotesTable migration
+class AddNoteableIdNoteableTypeAndIdIndexInNotesTable < Gitlab::Database::Migration[2.2]
+ milestone '17.10'
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_notes_on_noteable_id_noteable_type_and_id'
+
+ def up
+ # rubocop:disable Migration/PreventIndexCreation -- index prepared in advance
+ add_concurrent_index :notes, [:noteable_id, :noteable_type, :id], name: INDEX_NAME
+ # rubocop:enable Migration/PreventIndexCreation
+ end
+
+ def down
+ remove_concurrent_index_by_name :notes, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20250214085204_fill_ai_duo_chat_events_organization_id.rb b/db/post_migrate/20250214085204_fill_ai_duo_chat_events_organization_id.rb
new file mode 100644
index 00000000000..4b16c247d51
--- /dev/null
+++ b/db/post_migrate/20250214085204_fill_ai_duo_chat_events_organization_id.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class FillAiDuoChatEventsOrganizationId < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+ milestone '17.10'
+
+ def up
+ return unless Gitlab.ee? # Only EE has proper table partitions and data.
+
+ chat_events = define_batchable_model(:ai_duo_chat_events)
+
+ chat_events.each_batch(of: 1000, column: :id) do |batch|
+ batch.where(organization_id: nil).update_all(organization_id: 1) # DEFAULT_ORGANIZATION_ID
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20250220231747_finalize_hk_backfill_protected_environment_approval_rules_protected_environmen.rb b/db/post_migrate/20250220231747_finalize_hk_backfill_protected_environment_approval_rules_protected_environmen.rb
new file mode 100644
index 00000000000..55107f77d71
--- /dev/null
+++ b/db/post_migrate/20250220231747_finalize_hk_backfill_protected_environment_approval_rules_protected_environmen.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class FinalizeHkBackfillProtectedEnvironmentApprovalRulesProtectedEnvironmen < Gitlab::Database::Migration[2.2]
+ milestone '17.10'
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'BackfillProtectedEnvironmentApprovalRulesProtectedEnvironmentProjectId',
+ table_name: :protected_environment_approval_rules,
+ column_name: :id,
+ job_arguments: [:protected_environment_project_id, :protected_environments, :project_id,
+ :protected_environment_id],
+ finalize: true
+ )
+ end
+
+ def down; end
+end
diff --git a/db/schema_migrations/20250210065034 b/db/schema_migrations/20250210065034
new file mode 100644
index 00000000000..301d194c152
--- /dev/null
+++ b/db/schema_migrations/20250210065034
@@ -0,0 +1 @@
+17898021bbd8b5bdef675c73e0aef30a77b0cbbeb6348b89e75f2b4837ec58b4
\ No newline at end of file
diff --git a/db/schema_migrations/20250212110138 b/db/schema_migrations/20250212110138
new file mode 100644
index 00000000000..fac1f650dc3
--- /dev/null
+++ b/db/schema_migrations/20250212110138
@@ -0,0 +1 @@
+1d540f78bcd88abb82caedee131a8aa7cc73c1900784af93e797162e6f0edd51
\ No newline at end of file
diff --git a/db/schema_migrations/20250212132647 b/db/schema_migrations/20250212132647
new file mode 100644
index 00000000000..0d149aeb0af
--- /dev/null
+++ b/db/schema_migrations/20250212132647
@@ -0,0 +1 @@
+5fc9dfad9645f9d0a955e37a7fe1702e572701226ec1bdbfba754e1455e52d1a
\ No newline at end of file
diff --git a/db/schema_migrations/20250213125548 b/db/schema_migrations/20250213125548
new file mode 100644
index 00000000000..becc7259e99
--- /dev/null
+++ b/db/schema_migrations/20250213125548
@@ -0,0 +1 @@
+f6e2391f8d78b18c53b5c6e4e44fb93c59355badb4bc2aedbfdcb5f6f44b1548
\ No newline at end of file
diff --git a/db/schema_migrations/20250214085204 b/db/schema_migrations/20250214085204
new file mode 100644
index 00000000000..3ab76e69c4c
--- /dev/null
+++ b/db/schema_migrations/20250214085204
@@ -0,0 +1 @@
+7984c710d624f787cf69ba62ec525875ffcf7bada3fa763966125c21c5259043
\ No newline at end of file
diff --git a/db/schema_migrations/20250220231747 b/db/schema_migrations/20250220231747
new file mode 100644
index 00000000000..b245af541d5
--- /dev/null
+++ b/db/schema_migrations/20250220231747
@@ -0,0 +1 @@
+f471852ecae0f6af89d72a75897d3c13cebe57e25fdb509c1816d219d6c11cd5
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 663e6af3d2b..5b6ce385a00 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -3973,6 +3973,7 @@ CREATE TABLE ai_duo_chat_events (
event smallint NOT NULL,
namespace_path text,
payload jsonb,
+ organization_id bigint,
CONSTRAINT check_628cdfbf3f CHECK ((char_length(namespace_path) <= 255))
)
PARTITION BY RANGE ("timestamp");
@@ -13114,6 +13115,7 @@ CREATE TABLE draft_notes (
internal boolean DEFAULT false NOT NULL,
note_type smallint,
project_id bigint,
+ CONSTRAINT check_2a752d05fe CHECK ((project_id IS NOT NULL)),
CONSTRAINT check_c497a94a0e CHECK ((char_length(line_code) <= 255))
);
@@ -27268,9 +27270,6 @@ ALTER TABLE ONLY chat_names
ALTER TABLE ONLY chat_teams
ADD CONSTRAINT chat_teams_pkey PRIMARY KEY (id);
-ALTER TABLE draft_notes
- ADD CONSTRAINT check_2a752d05fe CHECK ((project_id IS NOT NULL)) NOT VALID;
-
ALTER TABLE workspaces
ADD CONSTRAINT check_2a89035b04 CHECK ((personal_access_token_id IS NOT NULL)) NOT VALID;
@@ -31511,6 +31510,8 @@ CREATE INDEX index_ai_conversation_threads_on_organization_id ON ai_conversation
CREATE INDEX index_ai_conversation_threads_on_user_id_and_last_updated_at ON ai_conversation_threads USING btree (user_id, last_updated_at);
+CREATE INDEX index_ai_duo_chat_events_on_organization_id ON ONLY ai_duo_chat_events USING btree (organization_id);
+
CREATE INDEX index_ai_duo_chat_events_on_personal_namespace_id ON ONLY ai_duo_chat_events USING btree (personal_namespace_id);
CREATE INDEX index_ai_duo_chat_events_on_user_id ON ONLY ai_duo_chat_events USING btree (user_id);
@@ -33959,6 +33960,8 @@ CREATE INDEX index_notes_on_namespace_id ON notes USING btree (namespace_id);
CREATE INDEX index_notes_on_noteable_id_and_noteable_type_and_system ON notes USING btree (noteable_id, noteable_type, system);
+CREATE INDEX index_notes_on_noteable_id_noteable_type_and_id ON notes USING btree (noteable_id, noteable_type, id);
+
CREATE INDEX index_notes_on_project_id_and_id_and_system_false ON notes USING btree (project_id, id) WHERE (NOT system);
CREATE INDEX index_notes_on_project_id_and_noteable_type ON notes USING btree (project_id, noteable_type);
diff --git a/doc/administration/gitlab_duo_self_hosted/troubleshooting.md b/doc/administration/gitlab_duo_self_hosted/troubleshooting.md
index 614ed4c3a16..bbcf0720682 100644
--- a/doc/administration/gitlab_duo_self_hosted/troubleshooting.md
+++ b/doc/administration/gitlab_duo_self_hosted/troubleshooting.md
@@ -92,7 +92,7 @@ We provide two debugging scripts to help administrators verify their self-hosted
```
For a `mixtral` model running on vLLM:
-
+
```shell
poetry run troubleshoot \
--model-family=mixtral \
@@ -114,6 +114,14 @@ Verify the output of the commands, and fix accordingly.
If both commands are successful, but GitLab Duo Code Suggestions is still not working,
raise an issue on the issue tracker.
+## GitLab Duo health check is not working
+
+When you [run a health check for GitLab Duo](../../user/gitlab_duo/setup.md#run-a-health-check-for-gitlab-duo), you might get an error like a `401 response from the AI gateway`.
+
+To resolve, first check if GitLab Duo features are functioning correctly. For example, send a message to Duo Chat.
+
+If this does not work, the error might be because of a known issue with GitLab Duo health check. For more information, see [issue 517097](https://gitlab.com/gitlab-org/gitlab/-/issues/517097).
+
## Check if GitLab can make a request to the model
From the GitLab Rails console, verify that GitLab can make a request to the model
diff --git a/doc/user/gitlab_duo/setup.md b/doc/user/gitlab_duo/setup.md
index 69c9a2d6f69..07fafc2ec6b 100644
--- a/doc/user/gitlab_duo/setup.md
+++ b/doc/user/gitlab_duo/setup.md
@@ -112,3 +112,5 @@ These tests are performed:
| Network | Tests whether your instance can connect to `customers.gitlab.com` and `cloud.gitlab.com`.
If your instance cannot connect to either destination, ensure that your firewall or proxy server settings [allow connection](setup.md). |
| Synchronization | Tests whether your subscription:
- Has been activated with an activation code and can be synchronized with `customers.gitlab.com`.
- Has correct access credentials.
- Has been synchronized recently. If it hasn't or the access credentials are missing or expired, you can [manually synchronize](../../subscriptions/self_managed/_index.md#manually-synchronize-subscription-data) your subscription data. |
| System exchange | Tests whether Code Suggestions can be used in your instance. If the system exchange assessment fails, users might not be able to use GitLab Duo features. |
+
+If you are experiencing any issues with the health check, see [GitLab Duo Self-Hosted troubleshooting](../../administration/gitlab_duo_self_hosted/troubleshooting.md#gitlab-duo-health-check-is-not-working).
diff --git a/doc/user/packages/yarn_repository/_index.md b/doc/user/packages/yarn_repository/_index.md
index 55055a128ae..aada5a45c88 100644
--- a/doc/user/packages/yarn_repository/_index.md
+++ b/doc/user/packages/yarn_repository/_index.md
@@ -5,347 +5,348 @@ info: To determine the technical writer assigned to the Stage/Group associated w
title: Publish packages with Yarn
---
-You can publish packages with [Yarn 1 (Classic)](https://classic.yarnpkg.com) and [Yarn 2+](https://yarnpkg.com).
+You can publish and install packages with [Yarn 1 (Classic)](https://classic.yarnpkg.com) and [Yarn 2+](https://yarnpkg.com).
-To find the Yarn version used in the deployment container, run `yarn --version` in the `script` block of the CI
+To find the Yarn version used in the deployment container, run `yarn --version` in the `script` block of the CI/CD
script job block that is responsible for calling `yarn publish`. The Yarn version is shown in the pipeline output.
-Learn how to build a [Yarn](../workflows/build_packages.md#yarn) package.
+## Authenticating to the package registry
-You can use the Yarn documentation to get started with
-[Yarn Classic](https://classic.yarnpkg.com/en/docs/getting-started) and
-[Yarn 2+](https://yarnpkg.com/getting-started).
-
-## Publish to GitLab package registry
-
-You can use Yarn to publish to the GitLab package registry.
-
-### Authentication to the package registry
-
-You need a token to publish a package. Different tokens are available depending on what you're trying to
+You need a token to interact with the package registry. Different tokens are available depending on what you're trying to
achieve. For more information, review the [guidance on tokens](../package_registry/_index.md#authenticate-with-the-registry).
- If your organization uses two-factor authentication (2FA), you must use a
- personal access token with the scope set to `api`.
-- If you publish a package via CI/CD pipelines, you can use a CI job token in
- private runners or you can register a variable for instance runners.
+ [personal access token](../../profile/personal_access_tokens.md) with the scope set to `api`.
+- If you publish a package with CI/CD pipelines, you can use a [CI/CD job token](../../../ci/jobs/ci_job_token.md) with
+ private runners. You can also [register a variable](https://docs.gitlab.com/runner/register/#register-with-a-runner-authentication-token) for instance runners.
-### Publish configuration
+### Configure Yarn for publication
-To publish, set the following configuration in `.yarnrc.yml`. This file should be
-located in the root directory of your package project source where `package.json` is found.
+To configure Yarn to publish to the package registry, edit your `.yarnrc.yml` file.
+You can find this file in root directory of your project, in the same place as the `package.json` file.
-```yaml
-npmScopes:
- :
- npmPublishRegistry: 'https:///api/v4/projects//packages/npm/'
- npmAlwaysAuth: true
- npmAuthToken: ''
-```
+- Edit `.yarnrc.yml` and add the following configuration:
-In this configuration:
+ ```yaml
+ npmScopes:
+ :
+ npmPublishRegistry: 'https:///api/v4/projects//packages/npm/'
+ npmAlwaysAuth: true
+ npmAuthToken: ''
+ ```
-- Replace `` with your organization scope, excluding the `@` symbol.
-- Replace `` with your domain name.
-- Replace `` with your project's ID, which you can find on the [project overview page](../../project/working_with_projects.md#access-a-project-by-using-the-project-id).
-- Replace `` with a deployment token, group access token, project access token, or personal access token.
+ In this configuration:
-Scoped registry does not work in Yarn Classic in `package.json` file, based on
-this [issue](https://github.com/yarnpkg/yarn/pull/7829).
-Therefore, under `publishConfig` there should be `registry` and not `@scope:registry` for Yarn Classic.
-You can publish using your command line or a CI/CD pipeline to the GitLab package registry.
+ - Replace `` with your organization scope. Do not include the `@` symbol.
+ - Replace `` with your domain name.
+ - Replace `` with your project's ID, which you can find on the [project overview page](../../project/working_with_projects.md#access-a-project-by-using-the-project-id).
+ - Replace `` with a deployment token, group access token, project access token, or personal access token.
-### Publishing via the command line - Manual Publish
+In Yarn Classic, scoped registries with `publishConfig["@scope:registry"]` are not supported. See [Yarn pull request 7829](https://github.com/yarnpkg/yarn/pull/7829) for more information.
+Instead, set `publishConfig` to `registry` in your `package.json` file.
-```shell
-# Yarn 1 (Classic)
-yarn publish
+## Publish a package
-# Yarn 2+
-yarn npm publish
-```
+You can publish a package from the command line, or with GitLab CI/CD.
-Your package should now publish to the package registry.
+### With the command line
-### Publishing via a CI/CD pipeline - Automated Publish
+To publish a package manually:
-You can use pipeline variables when you use this method.
+- Run the following command:
-You can use **instance runners** *(Default)* or **Private Runners** (Advanced).
+ ```shell
+ # Yarn 1 (Classic)
+ yarn publish
-#### Instance runners
+ # Yarn 2+
+ yarn npm publish
+ ```
-To create an authentication token for your project or group:
+### With CI/CD
-1. On the left sidebar, select **Search or go to** and find your project or group.
-1. On the left sidebar, select **Settings > Repository > Deploy Tokens**.
-1. Create a deployment token with `read_package_registry` and `write_package_registry` scopes and copy the generated token.
-1. On the left sidebar, select **Settings > CI/CD > Variables**.
-1. Select `Add variable` and use the following settings:
+You can publish a package automatically with instance runners (default) or private runners (advanced).
+You can use pipeline variables when you publish with CI/CD.
-| Field | Value |
-|--------------------|------------------------------|
-| key | `NPM_AUTH_TOKEN` |
-| value | `` |
-| type | Variable |
-| Protected variable | `CHECKED` |
-| Mask variable | `CHECKED` |
-| Expand variable | `CHECKED` |
+{{< tabs >}}
-To use any **Protected variable**:
+{{< tab title="Instance runners" >}}
+
+1. Create an authentication token for your project or group:
+
+ 1. On the left sidebar, select **Search or go to** and find your project or group.
+ 1. On the left sidebar, select **Settings > Repository > Deploy Tokens**.
+ 1. Create a deployment token with `read_package_registry` and `write_package_registry` scopes and copy the generated token.
+ 1. On the left sidebar, select **Settings > CI/CD > Variables**.
+ 1. Select `Add variable` and use the following settings:
+
+ | Field | Value |
+ |--------------------|------------------------------|
+ | key | `NPM_AUTH_TOKEN` |
+ | value | `` |
+ | type | Variable |
+ | Protected variable | `CHECKED` |
+ | Mask variable | `CHECKED` |
+ | Expand variable | `CHECKED` |
+
+1. Optional. To use protected variables:
1. Go to the repository that contains the Yarn package source code.
1. On the left sidebar, select **Settings > Repository**.
- If you are building from branches with tags, select **Protected Tags** and add `v*` (wildcard) for semantic versioning.
- If you are building from branches without tags, select **Protected Branches**.
-Then add the `NPM_AUTH_TOKEN` created above, to the `.yarnrc.yml` configuration
+1. Add the `NPM_AUTH_TOKEN` you created to the `.yarnrc.yml` configuration
in your package project root directory where `package.json` is found:
-```yaml
-npmScopes:
- :
- npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
- npmAlwaysAuth: true
- npmAuthToken: "${NPM_AUTH_TOKEN}"
-```
+ ```yaml
+ npmScopes:
+ :
+ npmPublishRegistry: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/'
+ npmAlwaysAuth: true
+ npmAuthToken: '${NPM_AUTH_TOKEN}'
+ ```
-In this configuration, replace `` with your organization scope, excluding the `@` symbol.
+ In this configuration, replace `` with your organization scope, excluding the `@` symbol.
-#### Private runners
+{{< /tab >}}
-Add the `CI_JOB_TOKEN` to the `.yarnrc.yml` configuration in your package project
-root directory where `package.json` is found:
+{{< tab title="Private runners" >}}
-```yaml
-npmScopes:
- :
- npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
- npmAlwaysAuth: true
- npmAuthToken: "${CI_JOB_TOKEN}"
-```
+1. Add your `CI_JOB_TOKEN` to the `.yarnrc.yml` configuration in the root directory of your package project, where `package.json` is located:
-In this configuration, replace `` with your organization scope, excluding the `@` symbol.
+ ```yaml
+ npmScopes:
+ :
+ npmPublishRegistry: '${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/'
+ npmAlwaysAuth: true
+ npmAuthToken: '${CI_JOB_TOKEN}'
+ ```
-To publish the package using CI/CD pipeline, In the GitLab project that houses
-your `.yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example to trigger
-only on any tag push:
+ In this configuration, replace `` with your organization scope, excluding the `@` symbol.
-```yaml
-# Yarn 1
-image: node:lts
+1. In the GitLab project with your `.yarnrc.yml`, edit or create a `.gitlab-ci.yml` file.
+For example, to trigger only on any tag push:
-stages:
- - deploy
+ In Yarn 1:
+
+ ```yaml
+ image: node:lts
-rules:
-- if: $CI_COMMIT_TAG
+ stages:
+ - deploy
-deploy:
- stage: deploy
- script:
- - yarn publish
-```
+ rules:
+ - if: $CI_COMMIT_TAG
-```yaml
-# Yarn 2+
-image: node:lts
+ deploy:
+ stage: deploy
+ script:
+ - yarn publish
+ ```
-stages:
- - deploy
+ In Yarn 2 and higher:
-rules:
- - if: $CI_COMMIT_TAG
+ ```yaml
+ image: node:lts
-deploy:
- stage: deploy
- before_script:
- - corepack enable
- - yarn set version stable
- script:
- - yarn npm publish
-```
+ stages:
+ - deploy
-Your package should now publish to the package registry when the pipeline runs.
+ rules:
+ - if: $CI_COMMIT_TAG
+
+ deploy:
+ stage: deploy
+ before_script:
+ - corepack enable
+ - yarn set version stable
+ script:
+ - yarn npm publish
+ ```
+
+When the pipeline runs, your package is added to the package registry.
+
+{{< /tab >}}
+
+{{< /tabs >}}
## Install a package
-{{< alert type="note" >}}
+You can install from an instance or project. If multiple packages have the same name and version,
+only the most recently published package is retrieved when you install a package.
-If multiple packages have the same name and version, the most recently-published
-package is retrieved when you install a package.
+### Scoped package names
-{{< /alert >}}
+To install from an instance, a package must be named with a [scope](https://docs.npmjs.com/misc/scope/).
+You can set up the scope for your package in the `.yarnrc.yml` file and with the `publishConfig` option in the `package.json`.
+You don't need to follow package naming conventions if you install from a project or group.
-You can use one of two API endpoints to install packages:
+A package scope begins with a `@` and follows the format `@owner/package-name`:
-- **Instance-level**: Best used when working with many packages in an organization scope.
+- The `@owner` is the top-level project that hosts the packages, not the root of the project with the package source code.
+- The package name can be anything.
-- If you plan to install a package through the [instance level](#install-from-the-instance-level),
- then you must name your package with a [scope](https://docs.npmjs.com/misc/scope/).
- Scoped packages begin with a `@` and have the `@owner/package-name` format. You can set up
- the scope for your package in the `.yarnrc.yml` file and by using the `publishConfig`
- option in the `package.json`.
+For example:
-- The value used for the `@scope` is the organization root (top-level project) `...com/my-org`
- *(@my-org)* that hosts the packages, not the root of the project with the package's source code.
-- The scope is always lowercase.
-- The package name can be anything you want `@my-org/any-name`.
-
-- **Project-level**: For when you have a one-off package.
-
-If you plan to install a package through the [project level](#install-from-the-project-level),
-you do not have to adhere to the naming convention.
-
-| Project URL | Package registry | Organization Scope | Full package name |
+| Project URL | Package registry | Organization scope | Full package name |
|-------------------------------------------------------------------|----------------------|--------------------|-----------------------------|
| `https://gitlab.com///` | Package Name Example | `@my-org` | `@my-org/package-name` |
| `https://gitlab.com///` | Project Name | `@example-org` | `@example-org/project-name` |
-You can install from the instance level or from the project level.
+### Install from the instance
-The configurations for `.yarnrc.yml` can be added per package consuming project
-root where `package.json` is located, or you can use a global
-configuration located in your system user home directory.
+If you're working with many packages in the same organization scope, consider installing from the instance.
-### Install from the instance level
+1. Configure your organization scope. In your `.yarnrc.yml` file, add the following:
-Use these steps for global configuration in the `.yarnrc.yml` file:
+ ```yaml
+ npmScopes:
+ :
+ npmRegistryServer: 'https:///api/v4/packages/npm'
+ ```
-1. [Configure organization scope](#configure-organization-scope).
-1. [Set the registry](#set-the-registry).
+ - Replace `` with the root level group of the project you're installing to the package from excluding the `@` symbol.
+ - Replace `` with your domain name, for example, `gitlab.com`.
-#### Configure organization scope
+1. Optional. If your package is private, you must configure access to the package registry:
-```yaml
-npmScopes:
- :
- npmRegistryServer: "https:///api/v4/packages/npm"
-```
+ ```yaml
+ npmRegistries:
+ ///api/v4/packages/npm:
+ npmAlwaysAuth: true
+ npmAuthToken: ''
+ ```
-- Replace `` with the root level group of the project you're installing to the package from excluding the `@` symbol.
-- Replace `` with your domain name, for example, `gitlab.com`.
+ - Replace `` with your domain name, for example, `gitlab.com`.
+ - Replace `` with a deployment token (recommended), group access token, project access token, or personal access token.
-#### Set the registry
+1. [Install the package with Yarn](#install-with-yarn).
-Skip this step if your package is public not private.
+### Install from a group or project
-```yaml
-npmRegistries:
- ///api/v4/packages/npm:
- npmAlwaysAuth: true
- npmAuthToken: ""
-```
+If you have a one-off package, you can install it from a group or project.
-- Replace `` with your domain name, for example, `gitlab.com`.
-- Replace `` with a deployment token (recommended), group access token, project access token, or personal access token.
+{{< tabs >}}
-### Install from the group level
+{{< tab title="From a group" >}}
-Use these steps for global configuration in the `.yarnrc.yml` file:
+1. Configure the group scope. In your `.yarnrc.yml` file, add the following:
-1. [Configure group scope](#configure-group-scope)
-1. [Set the registry](#set-the-registry-group-level)
+ ```yaml
+ npmScopes:
+ :
+ npmRegistryServer: 'https:///api/v4/groups//-/packages/npm'
+ ```
-#### Configure group scope
+ - Replace `` with the top-level group that contains the group you want to install from. Exclude the `@` symbol.
+ - Replace `` with your domain name, for example, `gitlab.com`.
+ - Replace `` with your group ID, found on the [group overview page](../../group#access-a-group-by-using-the-group-id).
-```yaml
-npmScopes:
- :
- npmRegistryServer: "https:///api/v4/groups//-/packages/npm"
-```
+1. Optional. If your package is private, you must set the registry:
-- Replace `` with the root level group of the project you're installing to the package from excluding the `@` symbol.
-- Replace `` with your domain name, for example, `gitlab.com`.
-- Replace `` with your group ID, found on the [group overview page](../../group#access-a-group-by-using-the-group-id).
+ ```yaml
+ npmRegistries:
+ ///api/v4/groups//-/packages/npm:
+ npmAlwaysAuth: true
+ npmAuthToken: ""
+ ```
-#### Set the registry (group level)
+ - Replace `` with your domain name, for example, `gitlab.com`.
+ - Replace `` with a deployment token (recommended), group access token, project access token, or personal access token.
+ - Replace `` with your group ID, found on the [group overview page](../../group#access-a-group-by-using-the-group-id).
-```yaml
-npmRegistries:
- ///api/v4/groups//-/packages/npm:
- npmAlwaysAuth: true
- npmAuthToken: ""
-```
+1. [Install the package with Yarn](#install-with-yarn).
-- Replace `` with the root level group of the project you're installing to the package from excluding the `@` symbol.
-- Replace `` with your domain name, for example, `gitlab.com`.
-- Replace `` with your group ID, found on the [group overview page](../../group#access-a-group-by-using-the-group-id).
+{{< /tab >}}
-### Install from the project level
+{{< tab title="From a project" >}}
-Use these steps for each project in the `.yarnrc.yml` file:
+1. Configure the project scope. In your `.yarnrc.yml` file, add the following:
-1. [Configure project scope](#configure-project-scope).
-1. [Set the registry](#set-the-registry-project-level).
+ ```yaml
+ npmScopes:
+ :
+ npmRegistryServer: "https:///api/v4/projects//packages/npm"
+ ```
-#### Configure project scope
+ - Replace `` with the top-level group that contains the project you want to install from. Exclude the `@` symbol.
+ - Replace `` with your domain name, for example, `gitlab.com`.
+ - Replace `` with your project ID, found on the [project overview page](../../project/working_with_projects.md#access-a-project-by-using-the-project-id).
-```yaml
-npmScopes:
- :
- npmRegistryServer: "https:///api/v4/projects//packages/npm"
-```
+1. Optional. If your package is private, you must set the registry:
-- Replace `` with the root level group of the project you're installing to the package from excluding the `@` symbol.
-- Replace `` with your domain name, for example, `gitlab.com`.
-- Replace `` with your project ID, found on the [project overview page](../../project/working_with_projects.md#access-a-project-by-using-the-project-id).
+ ```yaml
+ npmRegistries:
+ ///api/v4/projects//packages/npm:
+ npmAlwaysAuth: true
+ npmAuthToken: ""
+ ```
-#### Set the registry (project level)
+ - Replace `` with your domain name, for example, `gitlab.com`.
+ - Replace `` with a deployment token (recommended), group access token, project access token, or personal access token.
+ - Replace `` with your project ID, found on the [project overview page](../../project/working_with_projects.md#access-a-project-by-using-the-project-id).
-Skip this step if your package is public not private.
+1. [Install the package with Yarn](#install-with-yarn).
-```yaml
-npmRegistries:
- ///api/v4/projects//packages/npm:
- npmAlwaysAuth: true
- npmAuthToken: ""
-```
+{{< /tab >}}
-- Replace `` with your domain name, for example, `gitlab.com`.
-- Replace `` with a deployment token (recommended), group access token, project access token, or personal access token.
-- Replace `` with your project ID, found on the [project overview page](../../project/working_with_projects.md#access-a-project-by-using-the-project-id).
+{{< /tabs >}}
-### Install the package
+### Install with Yarn
-For Yarn 2+, use `yarn add` either in the command line or in the CI/CD pipelines to install your packages:
+{{< tabs >}}
+
+{{< tab title="Yarn 2 or later" >}}
+
+- Run `yarn add` either from the command line, or from a CI/CD pipeline:
```shell
yarn add @scope/my-package
```
-#### For Yarn Classic
+{{< /tab >}}
-The Yarn Classic setup, requires both `.npmrc` and `.yarnrc` files as
-[mentioned in issue](https://github.com/yarnpkg/yarn/issues/4451#issuecomment-753670295):
+{{< tab title="Yarn Classic" >}}
-- Place credentials in the `.npmrc` file.
-- Place the scoped registry in the `.yarnrc` file.
+Yarn Classic requires both a `.npmrc` and a `.yarnrc` file.
+See [Yarn issue 4451](https://github.com/yarnpkg/yarn/issues/4451#issuecomment-753670295) for more information.
-```shell
-# .npmrc
-## Instance level
-///api/v4/packages/npm/:_authToken=""
-## Group level
-///api/v4/groups//-/packages/npm/:_authToken=""
-## Project level
-///api/v4/projects//packages/npm/:_authToken=""
+1. Place your credentials in the `.npmrc` file, and the scoped registry in the `.yarnrc` file:
-# .yarnrc
-## Instance level
-"@scope:registry" "https:///api/v4/packages/npm/"
-## Group level
-"@scope:registry" "https:///api/v4/groups//-/packages/npm/"
-## Project level
-"@scope:registry" "https:///api/v4/projects//packages/npm/"
-```
+ ```shell
+ # .npmrc
+ ## For the instance
+ ///api/v4/packages/npm/:_authToken=''
+ ## For the group
+ ///api/v4/groups//-/packages/npm/:_authToken=''
+ ## For the project
+ ///api/v4/projects//packages/npm/:_authToken=''
-Then you can use `yarn add` to install your packages.
+ # .yarnrc
+ ## For the instance
+ '@scope:registry' 'https:///api/v4/packages/npm/'
+ ## For the group
+ '@scope:registry' 'https:///api/v4/groups//-/packages/npm/'
+ ## For the project
+ '@scope:registry' 'https:///api/v4/projects//packages/npm/'
+ ```
+
+1. Run `yarn add` either from the command line, or from a CI/CD pipeline:
+
+ ```shell
+ yarn add @scope/my-package
+ ```
+
+{{< /tab >}}
+
+{{< /tabs >}}
## Related topics
-- [npm documentation](../npm_registry/_index.md#helpful-hints)
+- [npm package registry documentation](../npm_registry/_index.md#helpful-hints)
- [Yarn Migration Guide](https://yarnpkg.com/migration/guide)
+- [Build a Yarn package](../workflows/build_packages.md#yarn)
## Troubleshooting
@@ -365,11 +366,11 @@ info If you think this is a bug, please open a bug report with the information p
info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command
```
-In this case, the following commands creates a file called `.yarnrc` in the current directory. Make sure to be in either your user home directory for global configuration or your project root for per-project configuration:
+In this case, the following commands create a file called `.yarnrc` in the current directory. Make sure to be in either your user home directory for global configuration or your project root for per-project configuration:
```shell
-yarn config set '//gitlab.example.com/api/v4/projects//packages/npm/:_authToken' ""
-yarn config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' ""
+yarn config set '//gitlab.example.com/api/v4/projects//packages/npm/:_authToken' ''
+yarn config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' ''
```
### `yarn install` fails to clone repository as a dependency
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index adc44cf890c..6c6e66f3727 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9693,9 +9693,6 @@ msgstr ""
msgid "Billing|User successfully scheduled for removal. This process might take some time. Refresh the page to see the changes."
msgstr ""
-msgid "Billing|User was successfully removed"
-msgstr ""
-
msgid "Billing|You are about to remove user %{username} from your subscription. If you continue, the user will be removed from the %{namespace} group and all its subgroups and projects. This action can't be undone."
msgstr ""
@@ -19742,6 +19739,9 @@ msgstr ""
msgid "Dependency list"
msgstr ""
+msgid "DependencyListExport|License Identifiers"
+msgstr ""
+
msgid "DependencyListExport|Location"
msgstr ""
@@ -19751,9 +19751,18 @@ msgstr ""
msgid "DependencyListExport|Packager"
msgstr ""
+msgid "DependencyListExport|Project"
+msgstr ""
+
msgid "DependencyListExport|Version"
msgstr ""
+msgid "DependencyListExport|Vulnerabilities Detected"
+msgstr ""
+
+msgid "DependencyListExport|Vulnerability IDs"
+msgstr ""
+
msgid "DependencyProxy|%{docLinkStart}See the documentation%{docLinkEnd} for other ways to store Docker images in Dependency Proxy cache."
msgstr ""
@@ -64399,7 +64408,7 @@ msgstr ""
msgid "Vulnerability|The CVSS (Common Vulnerability Scoring System) is a standardized framework for assessing and communicating the severity of security vulnerabilities in software. It provides a numerical score (ranging from 0.0 to 10.0) to indicate the severity risk of the vulnerability."
msgstr ""
-msgid "Vulnerability|The Exploit Prediction Scoring System model produces a probability score between 0 and 1 indicating the likelihood that a vulnerability will be exploited in the next 30 days."
+msgid "Vulnerability|The Exploit Prediction Scoring System model produces a percentage value between 0 and 100 that represents the likelihood that a vulnerability will be exploited in the next 30 days."
msgstr ""
msgid "Vulnerability|The scanner determined this vulnerability to be a false positive. Verify the evaluation before changing its status. %{linkStart}Learn more about false positive detection.%{linkEnd}"
diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb
index 15f31f44b5c..10108bd366c 100644
--- a/qa/qa/service/docker_run/gitlab_runner.rb
+++ b/qa/qa/service/docker_run/gitlab_runner.rb
@@ -115,7 +115,7 @@ module QA
def runner_auth_token
runner_list = shell("docker exec #{@name} sh -c 'gitlab-runner list'")
- runner_list.match(/Token\e\[0;m=([a-zA-Z0-9_-]+)/i)&.[](1)
+ runner_list.match(/Token\e\[0;m=([^ ]+)/)&.[](1)
end
def unregister_command
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
index 4b72524d66d..71d1ee058ea 100644
--- a/qa/spec/page/element_spec.rb
+++ b/qa/spec/page/element_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe QA::Page::Element do
subject { described_class.new(:something) }
it 'has no attribute[pattern]' do
- expect(subject.attributes[:pattern]).to be(nil)
+ expect(subject.attributes[:pattern]).to be_nil
end
it 'is not required by default' do
diff --git a/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb b/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb
index 5488dca4c40..f75c31f2fc2 100644
--- a/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb
+++ b/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb
@@ -52,7 +52,7 @@ module QA
end
it 'resolving the registry returns nil' do
- expect(service.third_party_registry).to be(nil)
+ expect(service.third_party_registry).to be_nil
end
it 'throws if environment is missing' do
diff --git a/qa/spec/service/shellout_spec.rb b/qa/spec/service/shellout_spec.rb
index 52f095f165a..393615cc462 100644
--- a/qa/spec/service/shellout_spec.rb
+++ b/qa/spec/service/shellout_spec.rb
@@ -40,7 +40,7 @@ module QA
expect(wait_thread).to receive(:value).twice.and_return(non_errored_wait)
subject.shell('docker login -u user -p secret', mask_secrets: %w[secret user]) do |output|
- expect(output).not_to be(nil)
+ expect(output).not_to be_nil
expect(output).to eql('logged in as **** with password ****')
end
end
diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt
index a1b3b829a37..8e07546608b 100644
--- a/scripts/frontend/quarantined_vue3_specs.txt
+++ b/scripts/frontend/quarantined_vue3_specs.txt
@@ -30,7 +30,6 @@ ee/spec/frontend/boards/components/epic_board_content_sidebar_spec.js
ee/spec/frontend/boards/components/epics_swimlanes_spec.js
ee/spec/frontend/ci/pipeline_details/header/pipeline_header_spec.js
ee/spec/frontend/ci/runner/components/runner_usage_spec.js
-ee/spec/frontend/ci/secrets/components/secrets_app_spec.js
ee/spec/frontend/ci/secrets/components/secrets_breadcrumbs_spec.js
ee/spec/frontend/ci/secrets/router_spec.js
ee/spec/frontend/compliance_dashboard/components/frameworks_report/edit_framework/components/policies_section_spec.js
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index bd4d38bddd2..221b3997512 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -331,8 +331,8 @@ RSpec.describe ObjectStoreSettings, feature_category: :shared do
expect(settings['enabled']).to be false
expect(settings['direct_upload']).to be true
- expect(settings['remote_directory']).to be nil
- expect(settings['bucket_prefix']).to be nil
+ expect(settings['remote_directory']).to be_nil
+ expect(settings['bucket_prefix']).to be_nil
end
it 'respects original values' do
@@ -346,7 +346,7 @@ RSpec.describe ObjectStoreSettings, feature_category: :shared do
expect(settings['enabled']).to be true
expect(settings['direct_upload']).to be true
expect(settings['remote_directory']).to eq 'artifacts'
- expect(settings['bucket_prefix']).to be nil
+ expect(settings['bucket_prefix']).to be_nil
end
it 'supports bucket prefixes' do
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 2edd4d88b9c..18369ee2b39 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -775,7 +775,7 @@ RSpec.describe ApplicationController, feature_category: :shared do
it 'sets stream headers', :aggregate_failures do
subject
- expect(response.headers['Content-Length']).to be nil
+ expect(response.headers['Content-Length']).to be_nil
expect(response.headers['X-Accel-Buffering']).to eq 'no'
expect(response.headers['Last-Modified']).to eq '0'
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 2f3a076a30b..f75eb30c558 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe 'Database schema',
abuse_reports: %w[reporter_id user_id],
abuse_report_notes: %w[discussion_id],
ai_code_suggestion_events: %w[user_id],
- ai_duo_chat_events: %w[user_id],
+ ai_duo_chat_events: %w[user_id organization_id],
application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_app_id eks_account_id
eks_access_key_id],
approvals: %w[user_id project_id],
diff --git a/spec/dot_gitlab_ci/ci_configuration_validation/shared_context_and_examples.rb b/spec/dot_gitlab_ci/ci_configuration_validation/shared_context_and_examples.rb
index 0ea76a57f14..a3c6e84ddca 100644
--- a/spec/dot_gitlab_ci/ci_configuration_validation/shared_context_and_examples.rb
+++ b/spec/dot_gitlab_ci/ci_configuration_validation/shared_context_and_examples.rb
@@ -104,7 +104,7 @@ end
RSpec.shared_examples 'default branch pipeline' do
it 'is valid' do
- expect(pipeline.yaml_errors).to be nil
+ expect(pipeline.yaml_errors).to be_nil
expect(pipeline.errors).to be_empty
expect(pipeline.status).to eq('created')
expect(jobs).to include(expected_job_name)
@@ -113,7 +113,7 @@ end
RSpec.shared_examples 'merge request pipeline' do
it "succeeds with expected job" do
- expect(pipeline.yaml_errors).to be nil
+ expect(pipeline.yaml_errors).to be_nil
expect(pipeline.errors).to be_empty
expect(pipeline.status).to eq('created')
expect(jobs).to include(expected_job_name)
@@ -124,7 +124,7 @@ RSpec.shared_examples 'merge train pipeline' do
let(:ci_merge_request_event_type) { 'merge_train' }
it "succeeds with expected job" do
- expect(pipeline.yaml_errors).to be nil
+ expect(pipeline.yaml_errors).to be_nil
expect(pipeline.errors).to be_empty
expect(pipeline.status).to eq('created')
expect(jobs).to include('pre-merge-checks')
diff --git a/spec/features/admin/users/admin_impersonates_user_spec.rb b/spec/features/admin/users/admin_impersonates_user_spec.rb
index e37b4bf1562..524f257bf78 100644
--- a/spec/features/admin/users/admin_impersonates_user_spec.rb
+++ b/spec/features/admin/users/admin_impersonates_user_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe 'Admin impersonates user', feature_category: :user_management do
subject
icon = first('[data-testid="incognito-icon"]')
- expect(icon).not_to be nil
+ expect(icon).not_to be_nil
end
context 'when viewing the confirm email warning', :js do
diff --git a/spec/finders/container_repositories_finder_spec.rb b/spec/finders/container_repositories_finder_spec.rb
index 472c39d1f23..b1924d05dad 100644
--- a/spec/finders/container_repositories_finder_spec.rb
+++ b/spec/finders/container_repositories_finder_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe ContainerRepositoriesFinder do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
end
- it { is_expected.to be nil }
+ it { is_expected.to be_nil }
end
end
@@ -119,13 +119,13 @@ RSpec.describe ContainerRepositoriesFinder do
context 'when subject_type is group' do
let(:subject_type) { group }
- it { is_expected.to be nil }
+ it { is_expected.to be_nil }
end
context 'when subject_type is project' do
let(:subject_type) { project }
- it { is_expected.to be nil }
+ it { is_expected.to be_nil }
end
end
end
diff --git a/spec/finders/uploader_finder_spec.rb b/spec/finders/uploader_finder_spec.rb
index 875c6f0dd30..248c7366cbe 100644
--- a/spec/finders/uploader_finder_spec.rb
+++ b/spec/finders/uploader_finder_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe UploaderFinder, feature_category: :shared do
end
it 'returns nil' do
- expect(subject).to be(nil)
+ expect(subject).to be_nil
end
end
diff --git a/spec/frontend/credentials/components/credentials_filter_app_spec.js b/spec/frontend/credentials/components/credentials_filter_sort_app_spec.js
similarity index 50%
rename from spec/frontend/credentials/components/credentials_filter_app_spec.js
rename to spec/frontend/credentials/components/credentials_filter_sort_app_spec.js
index e684815ba8b..886eeadc6b4 100644
--- a/spec/frontend/credentials/components/credentials_filter_app_spec.js
+++ b/spec/frontend/credentials/components/credentials_filter_sort_app_spec.js
@@ -1,8 +1,10 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlFilteredSearch } from '@gitlab/ui';
-import CredentialsFilterApp from '~/credentials/components/credentials_filter_app.vue';
+import { GlFilteredSearch, GlSorting } from '@gitlab/ui';
+import CredentialsFilterSortApp from '~/credentials/components/credentials_filter_sort_app.vue';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { SORT_KEY_NAME } from '~/credentials/constants';
const mockFilters = [
'dummy',
@@ -35,15 +37,28 @@ jest.mock('~/lib/utils/url_utility', () => {
};
});
-describe('CredentialsFilterApp', () => {
+describe('CredentialsFilterSortApp', () => {
let wrapper;
+ const URL_HOST = 'https://localhost/';
const createComponent = () => {
- wrapper = shallowMount(CredentialsFilterApp);
+ wrapper = mount(CredentialsFilterSortApp, {
+ stubs: {
+ GlFilteredSearch: true,
+ },
+ });
};
+ beforeEach(() => {
+ setWindowLocation(URL_HOST);
+ });
+
const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
const findAvailableTokens = () => findFilteredSearch().props('availableTokens');
+ const findSortingComponent = () => wrapper.findComponent(GlSorting);
+ const findSortDirectionToggle = () =>
+ findSortingComponent().find('button[title^="Sort direction"]');
+ const findDropdownToggle = () => findSortingComponent().find('button[aria-haspopup="listbox"]');
describe('Mounts GlFilteredSearch with corresponding filters', () => {
it.each`
@@ -126,4 +141,92 @@ describe('CredentialsFilterApp', () => {
);
});
});
+ describe('renders CredentialsSortApp component', () => {
+ it('when url has filter param with value personal_access_tokens', async () => {
+ setWindowLocation('?filter=personal_access_tokens');
+ createComponent();
+ await nextTick();
+
+ expect(findSortingComponent().exists()).toBe(true);
+ });
+ it('when url has no filter param', async () => {
+ createComponent();
+ await nextTick();
+
+ expect(findSortingComponent().exists()).toBe(true);
+ });
+ });
+
+ describe('sort dropdown', () => {
+ it('defaults to sorting by "Created date" in ascending order', async () => {
+ createComponent();
+ await nextTick();
+ expect(findSortingComponent().props('isAscending')).toBe(true);
+ expect(findDropdownToggle().text()).toBe('Expiration date');
+ });
+
+ it('sets the sort label correctly', () => {
+ setWindowLocation('?sort=name_asc');
+
+ createComponent();
+
+ expect(findDropdownToggle().text()).toBe('Name');
+ });
+
+ describe('new sort option is selected', () => {
+ beforeEach(async () => {
+ visitUrl.mockImplementation(() => {});
+ createComponent();
+
+ findSortingComponent().vm.$emit('sortByChange', SORT_KEY_NAME);
+ await nextTick();
+ });
+
+ it('sorts by new option', () => {
+ expect(visitUrl).toHaveBeenCalledWith(`${URL_HOST}?sort=name_asc`);
+ });
+ });
+ });
+
+ describe('sort direction toggle', () => {
+ beforeEach(() => {
+ visitUrl.mockImplementation(() => {});
+ });
+
+ describe('when current sort direction is ascending', () => {
+ beforeEach(() => {
+ setWindowLocation('?sort=name_asc');
+
+ createComponent();
+ });
+
+ describe('when sort direction toggle is clicked', () => {
+ beforeEach(() => {
+ findSortDirectionToggle().trigger('click');
+ });
+
+ it('sorts in descending order', () => {
+ expect(visitUrl).toHaveBeenCalledWith(`${URL_HOST}?sort=name_desc`);
+ });
+ });
+ });
+
+ describe('when current sort direction is descending', () => {
+ beforeEach(() => {
+ setWindowLocation('?sort=name_desc');
+
+ createComponent();
+ });
+
+ describe('when sort direction toggle is clicked', () => {
+ beforeEach(() => {
+ findSortDirectionToggle().trigger('click');
+ });
+
+ it('sorts in ascending order', () => {
+ expect(visitUrl).toHaveBeenCalledWith(`${URL_HOST}?sort=name_asc`);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/credentials/utils_spec.js b/spec/frontend/credentials/utils_spec.js
new file mode 100644
index 00000000000..19bde7f8b68
--- /dev/null
+++ b/spec/frontend/credentials/utils_spec.js
@@ -0,0 +1,11 @@
+import { buildSortedUrl } from '~/credentials/utils';
+
+describe('buildSortedUrl', () => {
+ it('builds correct URL for ascending sort', () => {
+ expect(buildSortedUrl('name', false)).toBe('http://test.host/?sort=name_desc');
+ });
+
+ it('builds correct URL for descending sort', () => {
+ expect(buildSortedUrl('created', true)).toBe('http://test.host/?sort=created_asc');
+ });
+});
diff --git a/spec/frontend/rapid_diffs/app/app_spec.js b/spec/frontend/rapid_diffs/app/app_spec.js
index 2c513c1422e..b5293880a9b 100644
--- a/spec/frontend/rapid_diffs/app/app_spec.js
+++ b/spec/frontend/rapid_diffs/app/app_spec.js
@@ -6,10 +6,10 @@ import { DiffFile } from '~/rapid_diffs/diff_file';
import { DiffFileMounted } from '~/rapid_diffs/diff_file_mounted';
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
import { pinia } from '~/pinia/instance';
-import { initFileBrowser } from '~/rapid_diffs/app/file_browser';
+import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser';
jest.mock('~/rapid_diffs/app/view_settings');
-jest.mock('~/rapid_diffs/app/file_browser');
+jest.mock('~/rapid_diffs/app/init_file_browser');
describe('Rapid Diffs App', () => {
let app;
diff --git a/spec/frontend/rapid_diffs/app/file_browser_spec.js b/spec/frontend/rapid_diffs/app/file_browser_spec.js
new file mode 100644
index 00000000000..56717b09b30
--- /dev/null
+++ b/spec/frontend/rapid_diffs/app/file_browser_spec.js
@@ -0,0 +1,53 @@
+import { shallowMount } from '@vue/test-utils';
+import FileBrowser from '~/rapid_diffs/app/file_browser.vue';
+import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
+import store from '~/mr_notes/stores';
+import * as types from '~/diffs/store/mutation_types';
+
+describe('FileBrowser', () => {
+ let wrapper;
+ let commit;
+
+ const createComponent = ({ loadedFiles = {}, ...rest } = {}) => {
+ wrapper = shallowMount(FileBrowser, {
+ store,
+ propsData: {
+ loadedFiles,
+ ...rest,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ commit = jest.spyOn(store, 'commit');
+ });
+
+ it('passes down loaded files', () => {
+ const loadedFiles = { foo: 1 };
+ createComponent({ loadedFiles });
+ expect(wrapper.findComponent(DiffsFileTree).props('loadedFiles')).toStrictEqual(loadedFiles);
+ });
+
+ it('is visible by default', () => {
+ createComponent();
+ expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(true);
+ });
+
+ it('toggles visibility', async () => {
+ createComponent();
+ await wrapper.findComponent(DiffsFileTree).vm.$emit('toggled');
+ expect(wrapper.findComponent(DiffsFileTree).props('visible')).toBe(false);
+ });
+
+ it('handles click', async () => {
+ const file = { fileHash: 'foo' };
+ createComponent();
+ await wrapper.findComponent(DiffsFileTree).vm.$emit('clickFile', file);
+ expect(wrapper.emitted('clickFile')).toStrictEqual([[file]]);
+ expect(commit).toHaveBeenCalledWith(
+ `diffs/${types.SET_CURRENT_DIFF_FILE}`,
+ file.fileHash,
+ undefined,
+ );
+ });
+});
diff --git a/spec/frontend/rapid_diffs/app/init_file_browser_spec.js b/spec/frontend/rapid_diffs/app/init_file_browser_spec.js
new file mode 100644
index 00000000000..ba92ba86e0c
--- /dev/null
+++ b/spec/frontend/rapid_diffs/app/init_file_browser_spec.js
@@ -0,0 +1,75 @@
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import store from '~/mr_notes/stores';
+import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser';
+import createEventHub from '~/helpers/event_hub_factory';
+import waitForPromises from 'helpers/wait_for_promises';
+import { DiffFile } from '~/rapid_diffs/diff_file';
+
+jest.mock('~/rapid_diffs/app/file_browser.vue', () => ({
+ props: jest.requireActual('~/rapid_diffs/app/file_browser.vue').default.props,
+ render(h) {
+ return h('div', {
+ attrs: {
+ 'data-file-browser-component': true,
+ 'data-loaded-files': JSON.stringify(this.loadedFiles),
+ },
+ on: {
+ click: () => {
+ this.$emit('clickFile', { fileHash: 'first' });
+ },
+ },
+ });
+ },
+}));
+
+describe('Init file browser', () => {
+ let dispatch;
+
+ const getMountElement = () => document.querySelector('[data-file-browser]');
+ const getFileBrowser = () => document.querySelector('[data-file-browser-component]');
+
+ beforeEach(() => {
+ dispatch = jest.spyOn(store, 'dispatch').mockResolvedValue();
+ window.mrTabs = { eventHub: createEventHub() };
+ setHTMLFixture(
+ `
+
+
+ `,
+ );
+ });
+
+ beforeAll(() => {
+ customElements.define('diff-file', DiffFile);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('sets metadata endpoint', () => {
+ initFileBrowser();
+ expect(store.state.diffs.endpointMetadata).toBe(getMountElement().dataset.metadataEndpoint);
+ });
+
+ it('fetches metadata', () => {
+ initFileBrowser();
+ expect(dispatch).toHaveBeenCalledWith('diffs/fetchDiffFilesMeta');
+ });
+
+ it('provides already loaded files', async () => {
+ initFileBrowser();
+ await waitForPromises();
+ expect(JSON.parse(getFileBrowser().dataset.loadedFiles)).toStrictEqual({ first: true });
+ });
+
+ it('handles file clicks', async () => {
+ const selectFile = jest.fn();
+ const spy = jest.spyOn(DiffFile, 'findByFileHash').mockReturnValue({ selectFile });
+ initFileBrowser();
+ await waitForPromises();
+ getFileBrowser().click();
+ expect(spy).toHaveBeenCalledWith('first');
+ expect(selectFile).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/rapid_diffs/diff_file_spec.js b/spec/frontend/rapid_diffs/diff_file_spec.js
index d09362add80..7e947f0cc3d 100644
--- a/spec/frontend/rapid_diffs/diff_file_spec.js
+++ b/spec/frontend/rapid_diffs/diff_file_spec.js
@@ -1,5 +1,6 @@
import { DiffFile } from '~/rapid_diffs/diff_file';
import IS from '~/rapid_diffs/intersection_observer';
+import { DIFF_FILE_MOUNTED } from '~/rapid_diffs/dom_events';
// We have to use var here because jest hoists mock calls, so let would be uninitialized at this point
// eslint-disable-next-line no-var
@@ -61,19 +62,36 @@ describe('DiffFile Web Component', () => {
invisible: jest.fn(),
mounted: jest.fn(),
});
- getWebComponentElement().mount();
});
it('observes diff element', () => {
+ getWebComponentElement().mount();
expect(IS.prototype.observe).toHaveBeenCalledWith(getWebComponentElement());
});
it('triggers mounted event', () => {
+ let emitted = false;
+ document.addEventListener(DIFF_FILE_MOUNTED, () => {
+ emitted = true;
+ });
+ getWebComponentElement().mount();
expect(adapter.mounted).toHaveBeenCalled();
expect(adapter.mounted.mock.instances[0]).toStrictEqual(getContext());
+ expect(emitted).toBe(true);
+ });
+
+ it('#selectFile', () => {
+ getWebComponentElement().mount();
+ const spy = jest.spyOn(getWebComponentElement(), 'scrollIntoView');
+ getWebComponentElement().selectFile();
+ expect(spy).toHaveBeenCalled();
});
describe('when visible', () => {
+ beforeEach(() => {
+ getWebComponentElement().mount();
+ });
+
it('handles all clicks', () => {
triggerVisibility(true);
getDiffElement().click();
@@ -102,11 +120,11 @@ describe('DiffFile Web Component', () => {
});
describe('static methods', () => {
- it('findByFileHash', () => {
+ it('#findByFileHash', () => {
expect(DiffFile.findByFileHash('fileHash')).toBeInstanceOf(DiffFile);
});
- it('getAll', () => {
+ it('#getAll', () => {
document.body.innerHTML = ``;
const instances = DiffFile.getAll();
expect(instances.length).toBe(2);
diff --git a/spec/graphql/mutations/issues/set_due_date_spec.rb b/spec/graphql/mutations/issues/set_due_date_spec.rb
index edf9981359e..44dd6c9d5f0 100644
--- a/spec/graphql/mutations/issues/set_due_date_spec.rb
+++ b/spec/graphql/mutations/issues/set_due_date_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Mutations::Issues::SetDueDate, feature_category: :api do
let(:due_date) { nil }
it 'updates due date to be nil' do
- expect(mutated_issue.due_date).to be nil
+ expect(mutated_issue.due_date).to be_nil
end
end
@@ -44,7 +44,7 @@ RSpec.describe Mutations::Issues::SetDueDate, feature_category: :api do
let(:due_date) { 'test' }
it 'updates due date to be nil' do
- expect(mutated_issue.due_date).to be nil
+ expect(mutated_issue.due_date).to be_nil
end
end
end
diff --git a/spec/graphql/resolvers/container_repositories_resolver_spec.rb b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
index d2d1d622cf4..7abdcdb2bbd 100644
--- a/spec/graphql/resolvers/container_repositories_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe Resolvers::ContainerRepositoriesResolver do
end
context 'with unauthorized user' do
- it { is_expected.to be nil }
+ it { is_expected.to be_nil }
end
end
end
diff --git a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
index 7e0e55e8d2a..05d4ff5c388 100644
--- a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe Resolvers::PaginatedTreeResolver, feature_category: :source_code_
end
it 'returns nil' do
- is_expected.to be(nil)
+ is_expected.to be_nil
end
end
@@ -67,7 +67,7 @@ RSpec.describe Resolvers::PaginatedTreeResolver, feature_category: :source_code_
end
it 'returns nil' do
- is_expected.to be(nil)
+ is_expected.to be_nil
end
end
diff --git a/spec/graphql/resolvers/tree_resolver_spec.rb b/spec/graphql/resolvers/tree_resolver_spec.rb
index 9eafd272771..c78f664a6ff 100644
--- a/spec/graphql/resolvers/tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/tree_resolver_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Resolvers::TreeResolver do
result = resolve_repository({ ref: "master" })
- expect(result).to be(nil)
+ expect(result).to be_nil
end
end
end
diff --git a/spec/graphql/resolvers/users/group_count_resolver_spec.rb b/spec/graphql/resolvers/users/group_count_resolver_spec.rb
index 47160a33646..b5afd5c1e97 100644
--- a/spec/graphql/resolvers/users/group_count_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/group_count_resolver_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Resolvers::Users::GroupCountResolver do
it do
result = batch_sync { resolve_group_count(user1, user2) }
- expect(result).to be nil
+ expect(result).to be_nil
end
end
@@ -50,7 +50,7 @@ RSpec.describe Resolvers::Users::GroupCountResolver do
it do
result = batch_sync { resolve_group_count(user1, nil) }
- expect(result).to be nil
+ expect(result).to be_nil
end
end
end
diff --git a/spec/lib/gitlab/database/sharding_key_spec.rb b/spec/lib/gitlab/database/sharding_key_spec.rb
index d115a280d0d..d656d92a7aa 100644
--- a/spec/lib/gitlab/database/sharding_key_spec.rb
+++ b/spec/lib/gitlab/database/sharding_key_spec.rb
@@ -186,6 +186,7 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
WHERE c.column_name = 'organization_id'
AND (fk.referenced_table_name = 'organizations' OR fk.referenced_table_name IS NULL)
AND (c.column_default IS NOT NULL OR c.is_nullable::boolean OR fk.name IS NULL OR NOT fk.is_valid)
+ AND (c.table_schema = 'public')
ORDER BY c.table_name;
SQL
@@ -202,7 +203,8 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
"oauth_openid_requests" => "https://gitlab.com/gitlab-org/gitlab/-/issues/496717",
"oauth_device_grants" => "https://gitlab.com/gitlab-org/gitlab/-/issues/496717",
"uploads" => "https://gitlab.com/gitlab-org/gitlab/-/issues/398199",
- "bulk_import_trackers" => "https://gitlab.com/gitlab-org/gitlab/-/issues/517823"
+ "bulk_import_trackers" => "https://gitlab.com/gitlab-org/gitlab/-/issues/517823",
+ "ai_duo_chat_events" => "https://gitlab.com/gitlab-org/gitlab/-/issues/516140"
}
has_lfk = ->(lfks) { lfks.any? { |k| k.options[:column] == 'organization_id' && k.to_table == 'organizations' } }
diff --git a/spec/requests/projects/merge_requests/diffs_stream_spec.rb b/spec/requests/projects/merge_requests/diffs_stream_spec.rb
index d6ee6488f75..17769f95f62 100644
--- a/spec/requests/projects/merge_requests/diffs_stream_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_stream_spec.rb
@@ -81,8 +81,8 @@ RSpec.describe 'Merge Requests Diffs stream', feature_category: :code_review_wor
it 'streams diffs except the offset' do
go(offset: offset)
- offset_file_identifier_hashes = diff_files.to_a.take(offset).map(&:file_identifier_hash)
- remaining_file_identifier_hashes = diff_files.to_a.slice(offset..).map(&:file_identifier_hash)
+ offset_file_identifier_hashes = diff_files.to_a.take(offset).map(&:file_hash)
+ remaining_file_identifier_hashes = diff_files.to_a.slice(offset..).map(&:file_hash)
expect(response).to have_gitlab_http_status(:success)
expect(response.body).not_to include(*offset_file_identifier_hashes)
diff --git a/spec/support/shared_examples/with_diffs_blobs_param.rb b/spec/support/shared_examples/with_diffs_blobs_param.rb
index d8e808fcd7f..3f6408bc638 100644
--- a/spec/support/shared_examples/with_diffs_blobs_param.rb
+++ b/spec/support/shared_examples/with_diffs_blobs_param.rb
@@ -7,7 +7,7 @@ RSpec.shared_examples 'with diffs_blobs param' do
go(diff_blobs: true)
expect(response).to have_gitlab_http_status(:success)
- expect(response.body).to include(*diff_files.to_a.map(&:file_identifier_hash))
+ expect(response.body).to include(*diff_files.to_a.map(&:file_hash))
end
end
@@ -17,8 +17,8 @@ RSpec.shared_examples 'with diffs_blobs param' do
it 'streams diffs except the offset' do
go(diff_blobs: true, offset: offset)
- offset_file_identifier_hashes = diff_files.to_a.take(offset).map(&:file_identifier_hash)
- remaining_file_identifier_hashes = diff_files.to_a.slice(offset..).map(&:file_identifier_hash)
+ offset_file_identifier_hashes = diff_files.to_a.take(offset).map(&:file_hash)
+ remaining_file_identifier_hashes = diff_files.to_a.slice(offset..).map(&:file_hash)
expect(response).to have_gitlab_http_status(:success)
expect(response.body).not_to include(*offset_file_identifier_hashes)
@@ -28,6 +28,6 @@ RSpec.shared_examples 'with diffs_blobs param' do
end
def file_identifier_hashes(diff)
- diff.diffs.diff_files.to_a.map(&:file_identifier_hash)
+ diff.diffs.diff_files.to_a.map(&:file_hash)
end
end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 92e7d0d0cee..2a8ba890053 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -22,16 +22,6 @@ RSpec.describe ProcessCommitWorker, feature_category: :source_code_management do
expect(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap.limit_for(worker: described_class)).to eq(1000)
end
- context 'when concurrency_limit_process_commit_worker is disabled' do
- before do
- stub_feature_flags(concurrency_limit_process_commit_worker: false)
- end
-
- it 'does not have a concurrency limit' do
- expect(::Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap.limit_for(worker: described_class)).to eq(0)
- end
- end
-
describe '#perform' do
subject(:perform) { worker.perform(project_id, user_id, commit.to_hash, default) }