diff --git a/app/assets/javascripts/vue_shared/access_tokens/components/access_token_table.vue b/app/assets/javascripts/vue_shared/access_tokens/components/access_token_table.vue index 99ac55fafff..9489e927a1b 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/components/access_token_table.vue +++ b/app/assets/javascripts/vue_shared/access_tokens/components/access_token_table.vue @@ -11,13 +11,13 @@ import { } from '@gitlab/ui'; import { mapActions } from 'pinia'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { fallsBefore, nWeeksAfter } from '~/lib/utils/datetime_utility'; import { __, s__, sprintf } from '~/locale'; import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import UserDate from '~/vue_shared/components/user_date.vue'; import { useAccessTokens } from '../stores/access_tokens'; +import { fifteenDaysFromNow } from '../utils'; const REVOKE = 'revoke'; const ROTATE = 'rotate'; @@ -85,7 +85,7 @@ export default { }, isExpiring(expiresAt) { if (expiresAt) { - return fallsBefore(new Date(expiresAt), nWeeksAfter(new Date(), 2)); + return expiresAt < fifteenDaysFromNow(); } return false; diff --git a/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js b/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js index 640bf8ba226..f529045d696 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js +++ b/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js @@ -11,7 +11,7 @@ import { import { joinPaths } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import { SORT_OPTIONS, DEFAULT_SORT } from '~/access_tokens/constants'; -import { serializeParams, update2WeekFromNow, updateUrlWithQueryParams } from '../utils'; +import { serializeParams, update15DaysFromNow, updateUrlWithQueryParams } from '../utils'; /** * @typedef {{type: string, value: {data: string, operator: string}}} Filter @@ -98,7 +98,7 @@ export const useAccessTokens = defineStore('accessTokens', { }, async fetchStatistics() { try { - const updatedFilters = update2WeekFromNow(); + const updatedFilters = update15DaysFromNow(); this.statistics = await Promise.all( updatedFilters.map(async (stat) => { const params = serializeParams(stat.filters); diff --git a/app/assets/javascripts/vue_shared/access_tokens/utils.js b/app/assets/javascripts/vue_shared/access_tokens/utils.js index 86818a00698..74ff26b44e6 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/utils.js +++ b/app/assets/javascripts/vue_shared/access_tokens/utils.js @@ -1,4 +1,4 @@ -import { getDateInFuture, nWeeksAfter, toISODateFormat } from '~/lib/utils/datetime_utility'; +import { getDateInFuture, nDaysAfter, toISODateFormat } from '~/lib/utils/datetime_utility'; import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { STATISTICS_CONFIG } from '~/access_tokens/constants'; @@ -42,14 +42,21 @@ export function serializeParams(filters, page = 1) { } /** - * Replace the 'DATE_HOLDER' string with a date 2 weeks in the future based on current time. + * Returns a date 15 days in the future based on current time in ISO format ('YYYY-MM-DD') */ -export function update2WeekFromNow(stats = STATISTICS_CONFIG) { +export function fifteenDaysFromNow() { + return toISODateFormat(nDaysAfter(new Date(), 15)); +} + +/** + * Replace the 'DATE_HOLDER' string with a date 15 days in the future based on current time. + */ +export function update15DaysFromNow(stats = STATISTICS_CONFIG) { const clonedStats = structuredClone(stats); clonedStats.forEach((stat) => { const filter = stat.filters.find((item) => item.value.data === 'DATE_HOLDER'); if (filter) { - filter.value.data = toISODateFormat(nWeeksAfter(new Date(), 2)); + filter.value.data = fifteenDaysFromNow(); } }); diff --git a/app/models/concerns/encrypted_user_password.rb b/app/models/concerns/encrypted_user_password.rb index 596f65c5dd5..96ce8ab337a 100644 --- a/app/models/concerns/encrypted_user_password.rb +++ b/app/models/concerns/encrypted_user_password.rb @@ -34,7 +34,17 @@ module EncryptedUserPassword @password = new_password # rubocop:disable Gitlab/ModuleWithInstanceVariables return unless new_password.present? - self.encrypted_password = hash_this_password(new_password) + # Use SafeRequestStore to cache the password hash during registration + # This prevents redundant bcrypt operations when the same password is set on multiple + # User objects during registration. Our analysis showed that two separate User objects + # are created during registration (one by BuildService and another by Devise), and + # both trigger expensive password hashing operations. By caching within the request, + # we reduce registration time by ~31% while maintaining security. + hash_key = "password_hash:#{Digest::SHA256.hexdigest(new_password.to_s)}" + + self.encrypted_password = Gitlab::SafeRequestStore.fetch(hash_key) do + hash_this_password(new_password) + end end private diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml index da8fb2ed86a..b0df772ffe7 100644 --- a/config/gitlab_loose_foreign_keys.yml +++ b/config/gitlab_loose_foreign_keys.yml @@ -446,6 +446,19 @@ organization_users: - table: users column: user_id on_delete: async_delete +p_ai_active_context_code_enabled_namespaces: + - table: ai_active_context_connections + column: connection_id + on_delete: async_delete +p_ai_active_context_code_repositories: + - table: projects + column: project_id + on_delete: update_column_to + target_column: state + target_value: 240 + - table: p_ai_active_context_code_enabled_namespaces + column: enabled_namespace_id + on_delete: async_nullify p_ci_build_names: - table: projects column: project_id diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb index a26ca2cf247..a4cb598c79a 100644 --- a/config/initializers/postgres_partitioning.rb +++ b/config/initializers/postgres_partitioning.rb @@ -55,6 +55,8 @@ if Gitlab.ee? Vulnerabilities::Archive, Vulnerabilities::ArchivedRecord, Vulnerabilities::ArchiveExport, + Ai::ActiveContext::Code::EnabledNamespace, + Ai::ActiveContext::Code::Repository, Ai::KnowledgeGraph::EnabledNamespace, Ai::KnowledgeGraph::Replica, Ai::KnowledgeGraph::Task diff --git a/db/docs/p_ai_active_context_code_enabled_namespaces.yml b/db/docs/p_ai_active_context_code_enabled_namespaces.yml new file mode 100644 index 00000000000..ceb37799121 --- /dev/null +++ b/db/docs/p_ai_active_context_code_enabled_namespaces.yml @@ -0,0 +1,13 @@ +--- +table_name: p_ai_active_context_code_enabled_namespaces +classes: +- Ai::ActiveContext::Code::EnabledNamespace +feature_categories: +- global_search +description: Namespaces where Code embeddings are enabled +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193015 +milestone: '18.2' +gitlab_schema: gitlab_main_cell +sharding_key: + namespace_id: namespaces +table_size: small diff --git a/db/docs/p_ai_active_context_code_repositories.yml b/db/docs/p_ai_active_context_code_repositories.yml new file mode 100644 index 00000000000..903fad7ab08 --- /dev/null +++ b/db/docs/p_ai_active_context_code_repositories.yml @@ -0,0 +1,11 @@ +--- +table_name: p_ai_active_context_code_repositories +classes: +- Ai::ActiveContext::Code::Repository +feature_categories: +- global_search +description: Projects where Code embeddings are enabled +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/193173 +milestone: '18.2' +gitlab_schema: gitlab_main_cell_local +table_size: small diff --git a/db/migrate/20250530110141_create_ai_active_context_code_enabled_namespaces.rb b/db/migrate/20250530110141_create_ai_active_context_code_enabled_namespaces.rb new file mode 100644 index 00000000000..36f9faea7ef --- /dev/null +++ b/db/migrate/20250530110141_create_ai_active_context_code_enabled_namespaces.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class CreateAiActiveContextCodeEnabledNamespaces < Gitlab::Database::Migration[2.3] + include Gitlab::Database::PartitioningMigrationHelpers + + disable_ddl_transaction! + milestone '18.2' + + TABLE_NAME = :p_ai_active_context_code_enabled_namespaces + PARTITION_SIZE = 2_000_000 + + def up + with_lock_retries do + create_table TABLE_NAME, + options: 'PARTITION BY RANGE (namespace_id)', + primary_key: [:id, :namespace_id], if_not_exists: true do |t| + t.bigserial :id, null: false + + t.bigint :namespace_id, null: false, + index: { name: 'idx_ai_active_context_code_enabled_namespaces_namespace_id' } + t.bigint :connection_id, null: false + + t.jsonb :metadata, null: false, default: {} + t.integer :state, limit: 2, null: false, default: 0 + t.timestamps_with_timezone null: false + + t.index [:connection_id, :namespace_id], unique: true, + name: 'idx_unique_ai_code_repository_connection_namespace_id' + end + end + + create_partitions + end + + def down + drop_table TABLE_NAME + end + + private + + def create_partitions + min_id = Namespace.connection + .select_value("select min_value from pg_sequences where sequencename = 'namespaces_id_seq'") || 1 + + max_id = Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do + Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do + define_batchable_model('namespaces', connection: connection).maximum(:id) || min_id + end + end + + create_int_range_partitions(TABLE_NAME, PARTITION_SIZE, min_id, max_id) + end +end diff --git a/db/migrate/20250530110142_add_foreign_key_to_ai_active_context_code_enabled_namespaces.rb b/db/migrate/20250530110142_add_foreign_key_to_ai_active_context_code_enabled_namespaces.rb new file mode 100644 index 00000000000..ec9291e3ee4 --- /dev/null +++ b/db/migrate/20250530110142_add_foreign_key_to_ai_active_context_code_enabled_namespaces.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddForeignKeyToAiActiveContextCodeEnabledNamespaces < Gitlab::Database::Migration[2.3] + include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers + + disable_ddl_transaction! + milestone '18.2' + + def up + add_concurrent_partitioned_foreign_key :p_ai_active_context_code_enabled_namespaces, :namespaces, + column: :namespace_id, on_delete: :cascade + end + + def down + remove_foreign_key :p_ai_active_context_code_enabled_namespaces, column: :namespace_id + end +end diff --git a/db/migrate/20250602104537_create_ai_active_context_code_repositories.rb b/db/migrate/20250602104537_create_ai_active_context_code_repositories.rb new file mode 100644 index 00000000000..6497eb7ecfe --- /dev/null +++ b/db/migrate/20250602104537_create_ai_active_context_code_repositories.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class CreateAiActiveContextCodeRepositories < Gitlab::Database::Migration[2.3] + include Gitlab::Database::PartitioningMigrationHelpers + + disable_ddl_transaction! + milestone '18.2' + + TABLE_NAME = :p_ai_active_context_code_repositories + PARTITION_SIZE = 2_000_000 + + def up + with_lock_retries do + create_table TABLE_NAME, + options: 'PARTITION BY RANGE (project_id)', + primary_key: [:id, :project_id], if_not_exists: true do |t| + t.bigserial :id, null: false + + t.bigint :project_id, null: false + t.bigint :connection_id + t.bigint :enabled_namespace_id, + index: { name: 'idx_p_ai_active_context_code_repositories_enabled_namespace_id' } + + t.jsonb :metadata, null: false, default: {} + t.text :last_commit, limit: 64 + t.integer :state, limit: 2, null: false, default: 0 + + t.datetime_with_timezone :indexed_at + t.timestamps_with_timezone null: false + + t.index [:connection_id, :project_id], unique: true, + name: 'idx_unique_ai_code_repository_connection_project_id' + + t.index [:project_id, :state], + name: 'idx_ai_code_repository_project_id_state' + end + end + + create_partitions + end + + def down + drop_table TABLE_NAME + end + + private + + def create_partitions + min_id = Project.connection + .select_value("select min_value from pg_sequences where sequencename = 'projects_id_seq'") || 1 + + max_id = Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas.with_suppressed do + Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do + define_batchable_model('projects', connection: connection).maximum(:id) || min_id + end + end + + create_int_range_partitions(TABLE_NAME, PARTITION_SIZE, min_id, max_id) + end +end diff --git a/db/migrate/20250603134512_add_foreign_key_to_ai_active_context_repositories.rb b/db/migrate/20250603134512_add_foreign_key_to_ai_active_context_repositories.rb new file mode 100644 index 00000000000..68be305fdea --- /dev/null +++ b/db/migrate/20250603134512_add_foreign_key_to_ai_active_context_repositories.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class AddForeignKeyToAiActiveContextRepositories < Gitlab::Database::Migration[2.3] + include Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers + + disable_ddl_transaction! + milestone '18.2' + + def up + add_concurrent_partitioned_foreign_key( + :p_ai_active_context_code_repositories, + :ai_active_context_connections, + column: :connection_id, + on_delete: :nullify + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + :p_ai_active_context_code_repositories, + column: :connection_id + ) + end + end +end diff --git a/db/migrate/20250603234231_track_ai_active_context_connections_deletions.rb b/db/migrate/20250603234231_track_ai_active_context_connections_deletions.rb new file mode 100644 index 00000000000..e1221053242 --- /dev/null +++ b/db/migrate/20250603234231_track_ai_active_context_connections_deletions.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class TrackAiActiveContextConnectionsDeletions < Gitlab::Database::Migration[2.3] + include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers + + milestone '18.2' + + def up + track_record_deletions(:ai_active_context_connections) + end + + def down + untrack_record_deletions(:ai_active_context_connections) + end +end diff --git a/db/migrate/20250609231223_track_ai_active_context_enabled_namespace_deletions.rb b/db/migrate/20250609231223_track_ai_active_context_enabled_namespace_deletions.rb new file mode 100644 index 00000000000..70c91665a72 --- /dev/null +++ b/db/migrate/20250609231223_track_ai_active_context_enabled_namespace_deletions.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class TrackAiActiveContextEnabledNamespaceDeletions < Gitlab::Database::Migration[2.3] + include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers + + milestone '18.2' + + def up + track_record_deletions(:p_ai_active_context_code_enabled_namespaces) + end + + def down + untrack_record_deletions(:p_ai_active_context_code_enabled_namespaces) + end +end diff --git a/db/schema_migrations/20250530110141 b/db/schema_migrations/20250530110141 new file mode 100644 index 00000000000..1645de36b0c --- /dev/null +++ b/db/schema_migrations/20250530110141 @@ -0,0 +1 @@ +d05865907e8bd55525a9e40b0334b55d19cad2f869a15e0a13d21c1aaec01827 \ No newline at end of file diff --git a/db/schema_migrations/20250530110142 b/db/schema_migrations/20250530110142 new file mode 100644 index 00000000000..591cab94af8 --- /dev/null +++ b/db/schema_migrations/20250530110142 @@ -0,0 +1 @@ +0d0f2893ed94af556733ff194ba3957662bb3238c7f51a19bf49aa2768f37939 \ No newline at end of file diff --git a/db/schema_migrations/20250602104537 b/db/schema_migrations/20250602104537 new file mode 100644 index 00000000000..1f828fbe805 --- /dev/null +++ b/db/schema_migrations/20250602104537 @@ -0,0 +1 @@ +03d9eed855490b51f780aaef8a27ee657fd2611d0c2a15be8d2ce9fe79f62195 \ No newline at end of file diff --git a/db/schema_migrations/20250603134512 b/db/schema_migrations/20250603134512 new file mode 100644 index 00000000000..44155fd7fc2 --- /dev/null +++ b/db/schema_migrations/20250603134512 @@ -0,0 +1 @@ +5fc016740a0c4208f2b2470da9f368d50452379204ca12935d656af515e38d5a \ No newline at end of file diff --git a/db/schema_migrations/20250603234231 b/db/schema_migrations/20250603234231 new file mode 100644 index 00000000000..e65591309bb --- /dev/null +++ b/db/schema_migrations/20250603234231 @@ -0,0 +1 @@ +15c197e8c7d77bfa204ac0d460e46704d744682ee3c17d693e24be593018dfc5 \ No newline at end of file diff --git a/db/schema_migrations/20250609231223 b/db/schema_migrations/20250609231223 new file mode 100644 index 00000000000..5d6652526c5 --- /dev/null +++ b/db/schema_migrations/20250609231223 @@ -0,0 +1 @@ +072f4f5d3f60c149c197ea6a6813953fbb911fb365351279e259dd1027497925 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 4cb6de7b647..95e468dc131 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -4933,6 +4933,32 @@ CREATE TABLE merge_request_commits_metadata ( ) PARTITION BY RANGE (project_id); +CREATE TABLE p_ai_active_context_code_enabled_namespaces ( + id bigint NOT NULL, + namespace_id bigint NOT NULL, + connection_id bigint NOT NULL, + metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + state smallint DEFAULT 0 NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +) +PARTITION BY RANGE (namespace_id); + +CREATE TABLE p_ai_active_context_code_repositories ( + id bigint NOT NULL, + project_id bigint NOT NULL, + connection_id bigint, + enabled_namespace_id bigint, + metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + last_commit text, + state smallint DEFAULT 0 NOT NULL, + indexed_at timestamp with time zone, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_b253d453c7 CHECK ((char_length(last_commit) <= 64)) +) +PARTITION BY RANGE (project_id); + CREATE TABLE p_batched_git_ref_updates_deletions ( id bigint NOT NULL, project_id bigint NOT NULL, @@ -18883,6 +18909,24 @@ CREATE SEQUENCE organizations_id_seq ALTER SEQUENCE organizations_id_seq OWNED BY organizations.id; +CREATE SEQUENCE p_ai_active_context_code_enabled_namespaces_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE p_ai_active_context_code_enabled_namespaces_id_seq OWNED BY p_ai_active_context_code_enabled_namespaces.id; + +CREATE SEQUENCE p_ai_active_context_code_repositories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE p_ai_active_context_code_repositories_id_seq OWNED BY p_ai_active_context_code_repositories.id; + CREATE SEQUENCE p_batched_git_ref_updates_deletions_id_seq START WITH 1 INCREMENT BY 1 @@ -27831,6 +27875,10 @@ ALTER TABLE ONLY organization_users ALTER COLUMN id SET DEFAULT nextval('organiz ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organizations_id_seq'::regclass); +ALTER TABLE ONLY p_ai_active_context_code_enabled_namespaces ALTER COLUMN id SET DEFAULT nextval('p_ai_active_context_code_enabled_namespaces_id_seq'::regclass); + +ALTER TABLE ONLY p_ai_active_context_code_repositories ALTER COLUMN id SET DEFAULT nextval('p_ai_active_context_code_repositories_id_seq'::regclass); + ALTER TABLE ONLY p_batched_git_ref_updates_deletions ALTER COLUMN id SET DEFAULT nextval('p_batched_git_ref_updates_deletions_id_seq'::regclass); ALTER TABLE ONLY p_catalog_resource_sync_events ALTER COLUMN id SET DEFAULT nextval('p_catalog_resource_sync_events_id_seq'::regclass); @@ -30552,6 +30600,12 @@ ALTER TABLE ONLY organization_users ALTER TABLE ONLY organizations ADD CONSTRAINT organizations_pkey PRIMARY KEY (id); +ALTER TABLE ONLY p_ai_active_context_code_enabled_namespaces + ADD CONSTRAINT p_ai_active_context_code_enabled_namespaces_pkey PRIMARY KEY (id, namespace_id); + +ALTER TABLE ONLY p_ai_active_context_code_repositories + ADD CONSTRAINT p_ai_active_context_code_repositories_pkey PRIMARY KEY (id, project_id); + ALTER TABLE ONLY p_batched_git_ref_updates_deletions ADD CONSTRAINT p_batched_git_ref_updates_deletions_pkey PRIMARY KEY (id, partition_id); @@ -33376,6 +33430,10 @@ CREATE INDEX idx_abuse_reports_user_id_status_and_category ON abuse_reports USIN CREATE INDEX idx_addon_purchases_on_last_refreshed_at_desc_nulls_last ON subscription_add_on_purchases USING btree (last_assigned_users_refreshed_at DESC NULLS LAST); +CREATE INDEX idx_ai_active_context_code_enabled_namespaces_namespace_id ON ONLY p_ai_active_context_code_enabled_namespaces USING btree (namespace_id); + +CREATE INDEX idx_ai_code_repository_project_id_state ON ONLY p_ai_active_context_code_repositories USING btree (project_id, state); + CREATE INDEX idx_alert_management_alerts_on_created_at_project_id_with_issue ON alert_management_alerts USING btree (created_at, project_id) WHERE (issue_id IS NOT NULL); CREATE INDEX idx_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id); @@ -33608,6 +33666,8 @@ CREATE INDEX idx_on_protected_branch ON approval_group_rules_protected_branches CREATE INDEX idx_open_issues_on_project_and_confidential_and_author_and_id ON issues USING btree (project_id, confidential, author_id, id) WHERE (state_id = 1); +CREATE INDEX idx_p_ai_active_context_code_repositories_enabled_namespace_id ON ONLY p_ai_active_context_code_repositories USING btree (enabled_namespace_id); + CREATE INDEX idx_p_ci_finished_pipeline_ch_sync_evts_on_project_namespace_id ON ONLY p_ci_finished_pipeline_ch_sync_events USING btree (project_namespace_id); CREATE INDEX idx_packages_debian_group_component_files_on_architecture_id ON packages_debian_group_component_files USING btree (architecture_id); @@ -33774,6 +33834,10 @@ CREATE INDEX idx_unarchived_occurrences_for_aggregation_severity_nulls_first ON CREATE UNIQUE INDEX idx_uniq_analytics_dashboards_pointers_on_project_id ON analytics_dashboards_pointers USING btree (project_id); +CREATE UNIQUE INDEX idx_unique_ai_code_repository_connection_namespace_id ON ONLY p_ai_active_context_code_enabled_namespaces USING btree (connection_id, namespace_id); + +CREATE UNIQUE INDEX idx_unique_ai_code_repository_connection_project_id ON ONLY p_ai_active_context_code_repositories USING btree (connection_id, project_id); + CREATE UNIQUE INDEX idx_usages_on_cmpt_used_by_project_cmpt_and_last_used_date ON catalog_resource_component_last_usages USING btree (component_id, used_by_project_id, last_used_date); CREATE INDEX idx_user_add_on_assignment_versions_on_item_id ON subscription_user_add_on_assignment_versions USING btree (item_id); @@ -41340,6 +41404,8 @@ ALTER INDEX index_uploads_9ba88c4165_on_uploaded_by_user_id ATTACH PARTITION vul ALTER INDEX index_uploads_9ba88c4165_on_uploader_and_path ATTACH PARTITION vulnerability_remediation_uploads_uploader_path_idx; +CREATE TRIGGER ai_active_context_connections_loose_fk_trigger AFTER DELETE ON ai_active_context_connections REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); + CREATE TRIGGER ai_conversation_threads_loose_fk_trigger AFTER DELETE ON ai_conversation_threads REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); CREATE TRIGGER approval_policy_rules_loose_fk_trigger AFTER DELETE ON approval_policy_rules REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); @@ -41404,6 +41470,8 @@ CREATE TRIGGER nullify_merge_request_metrics_build_data_on_update BEFORE UPDATE CREATE TRIGGER organizations_loose_fk_trigger AFTER DELETE ON organizations REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); +CREATE TRIGGER p_ai_active_context_code_enabled_namespaces_loose_fk_trigger AFTER DELETE ON p_ai_active_context_code_enabled_namespaces REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records(); + CREATE TRIGGER p_ci_builds_loose_fk_trigger AFTER DELETE ON p_ci_builds REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records_override_table('p_ci_builds'); CREATE TRIGGER p_ci_pipelines_loose_fk_trigger AFTER DELETE ON p_ci_pipelines REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records_override_table('p_ci_pipelines'); @@ -44750,6 +44818,9 @@ ALTER TABLE ONLY clusters_kubernetes_namespaces ALTER TABLE ONLY epic_issues ADD CONSTRAINT fk_rails_4209981af6 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; +ALTER TABLE p_ai_active_context_code_enabled_namespaces + ADD CONSTRAINT fk_rails_42b1b86224 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY analytics_cycle_analytics_stage_aggregations ADD CONSTRAINT fk_rails_42e6fe37d7 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -45659,6 +45730,9 @@ ALTER TABLE ONLY merge_request_predictions ALTER TABLE ONLY incident_management_escalation_rules ADD CONSTRAINT fk_rails_b3c9c17bd4 FOREIGN KEY (oncall_schedule_id) REFERENCES incident_management_oncall_schedules(id) ON DELETE CASCADE; +ALTER TABLE p_ai_active_context_code_repositories + ADD CONSTRAINT fk_rails_b3d72d06cf FOREIGN KEY (connection_id) REFERENCES ai_active_context_connections(id) ON DELETE SET NULL; + ALTER TABLE ONLY duo_workflows_checkpoints ADD CONSTRAINT fk_rails_b4c109b1a4 FOREIGN KEY (workflow_id) REFERENCES duo_workflows_workflows(id) ON DELETE CASCADE; diff --git a/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md b/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md index 3852ddf5c1f..e2eb22a1658 100644 --- a/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md +++ b/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md @@ -167,6 +167,12 @@ This change has been observed to notably improve response times in internal benc - [Supported foundation models in Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) + To set up the private Bedrock endpoint (operating in a VPC), ensure the `AWS_BEDROCK_RUNTIME_ENDPOINT` environment variable is configured with your internal URL when launching the AIGW container. + + **Example configuration**: `AWS_BEDROCK_RUNTIME_ENDPOINT = https://bedrock-runtime.{aws_region_name}.amazonaws.com` + + For VPC endpoints, the URL format may be different, such as `https://vpce-{vpc-endpoint-id}-{service-name}.{aws_region_name}.vpce.amazonaws.com` + 1. [Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/). Provides access to OpenAI's powerful models, enabling developers to integrate advanced AI capabilities into their applications with robust security and scalable infrastructure. - [Working with Azure OpenAI models](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/working-with-models?tabs=powershell) diff --git a/doc/ci/inputs/_index.md b/doc/ci/inputs/_index.md index 821d442ea50..2559d609e95 100644 --- a/doc/ci/inputs/_index.md +++ b/doc/ci/inputs/_index.md @@ -56,7 +56,7 @@ CI/CD Variables: ## Define input parameters with `spec:inputs` -Use `spec:inputs` in the CI/CD configuration [header](../yaml/_index.md) to define input parameters that +Use `spec:inputs` in the CI/CD configuration [header](../yaml/_index.md#header-keywords) to define input parameters that can be passed to the configuration file. Use the `$[[ inputs.input-id ]]` interpolation format outside the header section to declare where to use diff --git a/doc/development/fe_guide/dashboard_layout_framework.md b/doc/development/fe_guide/dashboard_layout_framework.md index e14fbfd5a17..2aa8ed2c0a9 100644 --- a/doc/development/fe_guide/dashboard_layout_framework.md +++ b/doc/development/fe_guide/dashboard_layout_framework.md @@ -59,7 +59,8 @@ customizable dashboard layout as part of GitLab issue [#546201](https://gitlab.c The `dashboard_layout.vue` component takes a dashboard configuration object as input and renders a dashboard layout with title, description, actions, and panels in a -cross-browser 12-column grid system. +cross-browser 12-column grid system. The grid is responsive and collapses down +to a single column at the [medium breakpoint](https://design.gitlab.com/product-foundations/layout/#breakpoints). ### Dashboard panels diff --git a/doc/integration/exact_code_search/zoekt.md b/doc/integration/exact_code_search/zoekt.md index e41cfcbd494..5d1fdf7d6ce 100644 --- a/doc/integration/exact_code_search/zoekt.md +++ b/doc/integration/exact_code_search/zoekt.md @@ -453,117 +453,3 @@ Alternatively, you can use the `gitlab:zoekt:info` Rake task. If the number of online nodes is lower than the number of configured nodes or is zero when nodes are configured, you might have connectivity issues between GitLab and your Zoekt nodes. - -### Debug Zoekt API connectivity - -You can test direct connectivity to Zoekt nodes and debug API responses by querying the Zoekt API directly through the Zoekt gateway using `curl` commands. - -GitLab uses different search methods depending on configuration: - -- **Direct node search**: Queries a specific Zoekt node directly. -- **Proxy search**: Uses the Zoekt proxy to distribute searches across multiple nodes (enabled with the [`zoekt_search_proxy` feature flag](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/171867)). - -To send a `curl` request to the Zoekt API: - -1. Run this command in the Toolbox pod: - - ```shell - kubectl exec -it -- bash - ``` - -1. Define the variables you need for the search: - - ```shell - # The Zoekt gateway requires auth - USERNAME=$(cat /etc/gitlab/zoekt/.gitlab_zoekt_username) - PASSWORD=$(cat /etc/gitlab/zoekt/.gitlab_zoekt_password) - - # Gateway endpoint - ZOEKT_GATEWAY_SERVICE_ENDPOINT="gitlab-gitlab-zoekt-gateway.default.svc.cluster.local:8080" - - # Search parameters - SEARCH_QUERY="string" - MAX_RESULTS=5000 - ``` - -1. Send a request to the Zoekt API through the gateway: - - - For direct node search, test connectivity to a specific Zoekt node: - - ```shell - # Define the specific Zoekt node to test - ZOEKT_POD_FQDN="gitlab-gitlab-zoekt-0.gitlab-gitlab-zoekt.default.svc.cluster.local" - - # Search all projects on a specific node - curl \ - http://$ZOEKT_GATEWAY_SERVICE_ENDPOINT/nodes/$ZOEKT_POD_FQDN/api/search \ - --request POST \ - --header "Content-Type: application/json" \ - --user "$USERNAME:$PASSWORD" \ - --write-out "\nGateway → Zoekt direct node: %{time_total}s | Status: %{http_code} | Size: %{size_download}B\n\n" \ - --data "{\"Q\": \"$SEARCH_QUERY\", \"Opts\": {\"TotalMaxMatchCount\": $MAX_RESULTS, \"NumContextLines\": 1}}" - - # Search specific projects only on a specific node - PROJECT_IDS='[1, 2, 3]' - curl \ - http://$ZOEKT_GATEWAY_SERVICE_ENDPOINT/nodes/$ZOEKT_POD_FQDN/api/search \ - --request POST \ - --header "Content-Type: application/json" \ - --user "$USERNAME:$PASSWORD" \ - --write-out "\nGateway → Zoekt direct node: %{time_total}s | Status: %{http_code} | Size: %{size_download}B\n\n" \ - --data "{\"Q\": \"$SEARCH_QUERY\", \"Opts\": {\"TotalMaxMatchCount\": $MAX_RESULTS, \"NumContextLines\": 1}, \"RepoIds\": $PROJECT_IDS}" - ``` - - - For proxy search, test multi-node search functionality when the `zoekt_search_proxy` feature flag is enabled: - - ```shell - # Define the individual Zoekt nodes for proxy forwarding - ZOEKT_POD_FQDN_1="gitlab-gitlab-zoekt-0.gitlab-gitlab-zoekt.default.svc.cluster.local" - ZOEKT_POD_FQDN_2="gitlab-gitlab-zoekt-1.gitlab-gitlab-zoekt.default.svc.cluster.local" - ZOEKT_POD_FQDN_3="gitlab-gitlab-zoekt-2.gitlab-gitlab-zoekt.default.svc.cluster.local" - - # Search all projects across multiple nodes using proxy - curl \ - http://$ZOEKT_GATEWAY_SERVICE_ENDPOINT/indexer/proxy_search \ - --request POST \ - --header "Content-Type: application/json" \ - --user "$USERNAME:$PASSWORD" \ - --write-out "\nGateway → Zoekt proxy: %{time_total}s | Status: %{http_code} | Size: %{size_download}B\n\n" \ - --data '{ - "Q": "'"$SEARCH_QUERY"'", - "Opts": {"TotalMaxMatchCount": '"$MAX_RESULTS"', "NumContextLines": 1}, - "ForwardTo": [ - {"Endpoint": "http://'"$ZOEKT_POD_FQDN_1"':8080/api/search"}, - {"Endpoint": "http://'"$ZOEKT_POD_FQDN_2"':8080/api/search"}, - {"Endpoint": "http://'"$ZOEKT_POD_FQDN_3"':8080/api/search"} - ] - }' - - # Search specific projects across multiple nodes using proxy - PROJECT_IDS='[1, 2, 3]' - curl \ - http://$ZOEKT_GATEWAY_SERVICE_ENDPOINT/indexer/proxy_search \ - --request POST \ - --header "Content-Type: application/json" \ - --user "$USERNAME:$PASSWORD" \ - --write-out "\nGateway → Zoekt proxy: %{time_total}s | Status: %{http_code} | Size: %{size_download}B\n\n" \ - --data '{ - "Q": "'"$SEARCH_QUERY"'", - "Opts": {"TotalMaxMatchCount": '"$MAX_RESULTS"', "NumContextLines": 1}, - "ForwardTo": [ - {"Endpoint": "http://'"$ZOEKT_POD_FQDN_1"':8080/api/search", "RepoIds": '"$PROJECT_IDS"'}, - {"Endpoint": "http://'"$ZOEKT_POD_FQDN_2"':8080/api/search", "RepoIds": '"$PROJECT_IDS"'}, - {"Endpoint": "http://'"$ZOEKT_POD_FQDN_3"':8080/api/search", "RepoIds": '"$PROJECT_IDS"'} - ] - }' - ``` - -#### Troubleshooting - -When you debug Zoekt API connectivity: - -- The `ZOEKT_GATEWAY_SERVICE_ENDPOINT` is the service endpoint for the Zoekt gateway and defaults to `gitlab-gitlab-zoekt-gateway.default.svc.cluster.local:8080`. -- For `ZOEKT_POD_FQDN`, `ZOEKT_POD_FQDN_1`, `ZOEKT_POD_FQDN_2` and `ZOEKT_POD_FQDN_3`, use the specific Zoekt pod FQDN. For example: `gitlab-gitlab-zoekt-0.gitlab-gitlab-zoekt.default.svc.cluster.local`. -- When GitLab performs Zoekt searches, it returns up to 5,000 results by default. -- For direct node searches, there may be multiple Zoekt pods in a StatefulSet (`gitlab-gitlab-zoekt-0`, `gitlab-gitlab-zoekt-1`, etc.). Test each Zoekt pod individually by updating the `ZOEKT_POD_FQDN` variable. -- For proxy searches, check the JSON response values for `Errors`, `Failures` and `TimedOut`. diff --git a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb index 089e525b83d..8771c73d340 100644 --- a/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb +++ b/lib/gitlab/analytics/cycle_analytics/aggregated/records_fetcher.rb @@ -8,7 +8,9 @@ module Gitlab include Gitlab::Utils::StrongMemoize include StageQueryHelpers - MAX_RECORDS = 20 + # Increased from 20 to 30 as part of gradual optimization to reduce API requests + # See https://gitlab.com/gitlab-org/gitlab/-/issues/543147 for context + MAX_RECORDS = 30 MAPPINGS = { Issue => { diff --git a/lib/gitlab/memory/reporter.rb b/lib/gitlab/memory/reporter.rb index 8d32745ac34..2c5541275e1 100644 --- a/lib/gitlab/memory/reporter.rb +++ b/lib/gitlab/memory/reporter.rb @@ -91,15 +91,20 @@ module Gitlab io_r.close err_w.close - report.run(io_w) - io_w.close + begin + report.run(io_w) + io_w.close + rescue Errno::EPIPE + # this means the process went away before we could write to it; + # fall through to standard error handling below + end _, status = Process.wait2(pid) end errors = err_r.read&.strip err_r.close - raise StandardError, "exit #{status.exitstatus}: #{errors}" if !status&.success? && errors.present? + raise StandardError, "exit #{status.exitstatus}: #{errors}" unless status&.success? ensure [io_r, io_w, err_r, err_w].each(&:close) # Make sure we don't leave any running processes behind. diff --git a/package.json b/package.json index aae08753870..e0e47d85f27 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@gitlab/duo-ui": "^8.18.0", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", - "@gitlab/query-language-rust": "0.8.4", + "@gitlab/query-language-rust": "0.8.5", "@gitlab/svgs": "3.134.0", "@gitlab/ui": "113.7.0", "@gitlab/vue-router-vue3": "npm:vue-router@4.5.1", diff --git a/qa/qa/tools/test_resource_data_processor.rb b/qa/qa/tools/test_resource_data_processor.rb index 7c72aa9ee00..ea720d85a45 100644 --- a/qa/qa/tools/test_resource_data_processor.rb +++ b/qa/qa/tools/test_resource_data_processor.rb @@ -30,6 +30,9 @@ module QA # @param [Integer] time fabrication time # @return [Hash] def collect(resource:, info:, fabrication_method:, fabrication_time:) + # http_method is empty when we use the browser_ui to create a resource, + # so assign it a default :post value if it is empty otherwise it will be set to :get in resource_api_path + http_method = resource.api_fabrication_http_method || :post api_path = resource_api_path(resource) type = resource.class.name @@ -38,7 +41,7 @@ module QA api_path: api_path, fabrication_method: fabrication_method, fabrication_time: fabrication_time, - http_method: resource.api_fabrication_http_method || :post, + http_method: http_method, timestamp: Time.now.to_s } end diff --git a/qa/spec/tools/test_resources_data_processor_spec.rb b/qa/spec/tools/test_resources_data_processor_spec.rb index e5f6ccb35cb..6ac31c5489b 100644 --- a/qa/spec/tools/test_resources_data_processor_spec.rb +++ b/qa/spec/tools/test_resources_data_processor_spec.rb @@ -38,6 +38,55 @@ RSpec.describe QA::Tools::TestResourceDataProcessor do it 'collects and stores resource' do expect(processor.resources).to eq(result) end + + context 'when fabrication_method is browser_ui' do + let(:method) { :browser_ui } + + let(:group_resource) do + group = instance_double(QA::Resource::Group, 'Group Resource') + allow(group).to receive(:class).and_return(QA::Resource::Group) + + # Start with nil + allow(group).to receive(:api_fabrication_http_method).and_return(nil) + + allow(group).to receive(:api_delete_path) do + # After this call, simulate that the method gets set to :get + allow(group).to receive(:api_fabrication_http_method).and_return(:get) + "/groups/123" + end + + allow(group).to receive(:respond_to?).with(:api_delete_path).and_return(true) + allow(group).to receive(:respond_to?).with(:api_get_path).and_return(true) + + group + end + + let(:expected_result_with_post) do + { + 'QA::Resource::Group' => [{ + info: info, + api_path: "/groups/123", + fabrication_method: method, + fabrication_time: time, + http_method: :post, + timestamp: Time.now.to_s + }] + } + end + + it 'defaults http_method to :post when api_fabrication_http_method is nil' do + new_processor = Class.new(described_class).instance + + new_processor.collect( + resource: group_resource, + info: info, + fabrication_method: method, + fabrication_time: time + ) + + expect(new_processor.resources).to eq(expected_result_with_post) + end + end end describe '.write_to_file' do diff --git a/spec/frontend/glql/core/parser/index_spec.js b/spec/frontend/glql/core/parser/index_spec.js index 2415171a896..e25ae85be51 100644 --- a/spec/frontend/glql/core/parser/index_spec.js +++ b/spec/frontend/glql/core/parser/index_spec.js @@ -58,6 +58,7 @@ describe('parse', () => { hasNextPage hasPreviousPage } + count } } ", @@ -122,6 +123,7 @@ assignee = currentUser()`), hasNextPage hasPreviousPage } + count } } ", @@ -187,6 +189,7 @@ query: assignee = currentUser() hasNextPage hasPreviousPage } + count } } ", diff --git a/spec/frontend/glql/core/parser/query_spec.js b/spec/frontend/glql/core/parser/query_spec.js index 40b5d66c06d..cae11fcd151 100644 --- a/spec/frontend/glql/core/parser/query_spec.js +++ b/spec/frontend/glql/core/parser/query_spec.js @@ -44,6 +44,7 @@ describe('GLQL Query Parser', () => { hasNextPage hasPreviousPage } + count } }" `); @@ -83,6 +84,7 @@ describe('GLQL Query Parser', () => { hasNextPage hasPreviousPage } + count } }" `); diff --git a/spec/frontend/vue_shared/access_tokens/components/access_token_table_spec.js b/spec/frontend/vue_shared/access_tokens/components/access_token_table_spec.js index ad3733959b3..05e56379ec7 100644 --- a/spec/frontend/vue_shared/access_tokens/components/access_token_table_spec.js +++ b/spec/frontend/vue_shared/access_tokens/components/access_token_table_spec.js @@ -123,7 +123,7 @@ describe('AccessTokenTable', () => { describe('when token is expiring', () => { it('shows an expiring status badge', () => { const tokens = [ - { ...defaultToken, expiresAt: new Date(Date.now() + 10 * 366000).toString() }, + { ...defaultToken, expiresAt: '2020-07-20' }, // 14 days ]; createComponent({ tokens }); @@ -250,7 +250,7 @@ describe('AccessTokenTable', () => { describe('when it is non-empty', () => { it('shows a relative date', () => { - const tokens = [{ ...defaultToken, expiresAt: '2021-01-01T00:00:00.000Z' }]; + const tokens = [{ ...defaultToken, expiresAt: '2021-01-01' }]; createComponent({ tokens }); const field = wrapper.findByTestId('field-expires'); diff --git a/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js b/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js index 1593b214a2a..e6eb9249457 100644 --- a/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js +++ b/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js @@ -1,7 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import { setActivePinia, createPinia } from 'pinia'; import { useAccessTokens } from '~/vue_shared/access_tokens/stores/access_tokens'; -import { update2WeekFromNow } from '~/vue_shared/access_tokens/utils'; +import { update15DaysFromNow } from '~/vue_shared/access_tokens/utils'; import { createAlert } from '~/alert'; import { smoothScrollTop } from '~/behaviors/smooth_scroll'; import axios from '~/lib/utils/axios_utils'; @@ -21,7 +21,7 @@ jest.mock('~/alert', () => ({ jest.mock('~/vue_shared/access_tokens/utils', () => ({ ...jest.requireActual('~/vue_shared/access_tokens/utils'), - update2WeekFromNow: jest.fn(), + update15DaysFromNow: jest.fn(), })); jest.mock('~/behaviors/smooth_scroll'); @@ -184,7 +184,7 @@ describe('useAccessTokens store', () => { const tooltipTitle = 'Filter for active tokens'; beforeEach(() => { store.setup({ filters, id, page, sorting, urlShow }); - update2WeekFromNow.mockReturnValueOnce([{ title, tooltipTitle, filters }]); + update15DaysFromNow.mockReturnValueOnce([{ title, tooltipTitle, filters }]); }); it('uses correct params in the fetch', async () => { diff --git a/spec/frontend/vue_shared/access_tokens/utils_spec.js b/spec/frontend/vue_shared/access_tokens/utils_spec.js index 820dc0b1590..260e4612ceb 100644 --- a/spec/frontend/vue_shared/access_tokens/utils_spec.js +++ b/spec/frontend/vue_shared/access_tokens/utils_spec.js @@ -1,7 +1,7 @@ import { defaultDate, serializeParams, - update2WeekFromNow, + update15DaysFromNow, updateUrlWithQueryParams, } from '~/vue_shared/access_tokens/utils'; import { getBaseURL, updateHistory } from '~/lib/utils/url_utility'; @@ -78,21 +78,21 @@ describe('update2WeekFromNow', () => { ]; it('replace `DATE_HOLDER` with date 2 weeks from now', () => { - expect(update2WeekFromNow(param)).toMatchObject([ + expect(update15DaysFromNow(param)).toMatchObject([ { title: 'dummy', tooltipTitle: 'dummy', - filters: [{ type: 'dummy', value: { data: '2020-07-20', operator: 'dummy' } }], + filters: [{ type: 'dummy', value: { data: '2020-07-21', operator: 'dummy' } }], }, ]); }); it('use default parameter', () => { - expect(update2WeekFromNow()).toBeDefined(); + expect(update15DaysFromNow()).toBeDefined(); }); it('returns a clone of the original parameter', () => { - const result = update2WeekFromNow(param); + const result = update15DaysFromNow(param); expect(result).not.toBe(param); expect(result[0].filters).not.toBe(param[0].filters); }); @@ -100,7 +100,7 @@ describe('update2WeekFromNow', () => { describe('updateUrlWithQueryParams', () => { it('calls updateHistory with correct parameters', () => { - updateUrlWithQueryParams({ params: { page: 1, revoked: true }, sort: 'name_asc' }); + updateUrlWithQueryParams({ params: { page: 1, revoked: 'true' }, sort: 'name_asc' }); expect(updateHistory).toHaveBeenCalledWith({ url: `${getBaseURL()}/?page=1&revoked=true&sort=name_asc`, diff --git a/spec/models/concerns/encrypted_user_password_spec.rb b/spec/models/concerns/encrypted_user_password_spec.rb index 58f2abbf2e6..b8bdf112e6b 100644 --- a/spec/models/concerns/encrypted_user_password_spec.rb +++ b/spec/models/concerns/encrypted_user_password_spec.rb @@ -124,9 +124,26 @@ RSpec.describe User, feature_category: :system_access do end describe '#password=' do - let(:user) { create(:user) } + let(:user) { build(:user) } let(:password) { described_class.random_password } + it 'reuses cached password hash when the same password is set again', :request_store do + user.password = password + original_result = user.encrypted_password + expect(user).not_to receive(:hash_this_password) + user.password = password + expect(user.encrypted_password).to eq(original_result) + end + + it 'computes a new hash when a different password is set', :request_store do + user.password = 's3cret' + original_result = user.encrypted_password + expect(user).to receive(:hash_this_password).with(password).and_call_original + user.password = password + expect(user.encrypted_password).to be_present + expect(user.encrypted_password).not_to eq(original_result) + end + def compare_bcrypt_password(user, password) Devise::Encryptor.compare(described_class, user.encrypted_password, password) end diff --git a/yarn.lock b/yarn.lock index 3565c57e0a6..ecbf8857964 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1438,10 +1438,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== -"@gitlab/query-language-rust@0.8.4": - version "0.8.4" - resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.8.4.tgz#5d38f17bc87a00c5fdb04db65beddbef99426bd4" - integrity sha512-sUS4IpUYAwTFfKNNx0y8Jz6TViYMaGN+DoYe+WiCwq30LuCHE2LPWkq3H218oeqYpPjhWKH28esraWGSDp940Q== +"@gitlab/query-language-rust@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.8.5.tgz#97e3778cd96c7bad4a28480ac243936fb0cbc7a1" + integrity sha512-a/q38sdafUHz+4rTxem3sQuWb6ilfsAWUC1t9qYo1ziyNnvoIZGZga2g1QjhNeUNj71qKQ6fkSSXWIQ9sH5VcQ== "@gitlab/stylelint-config@6.2.2": version "6.2.2"