Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-28 15:09:57 +00:00
parent 324185d93b
commit 82d72ee0ea
57 changed files with 1785 additions and 209 deletions

View File

@ -468,6 +468,21 @@
- !reference [.db-services-with-auto-explain, services]
- !reference [.es8-services, services]
.opensearch-latest-services:
services:
- !reference [.zoekt-services, services]
- name: opensearchproject/opensearch:latest
alias: elasticsearch
command: ["bin/opensearch", "-E", "discovery.type=single-node", "-E", "plugins.security.disabled=true", "-E", "cluster.routing.allocation.disk.threshold_enabled=false"]
.use-pg16-opensearch-latest-ee:
extends:
- .use-pg16
- .zoekt-variables
services:
- !reference [.db-services-with-auto-explain, services]
- !reference [.opensearch-latest-services, services]
.os1-services:
services:
- !reference [.zoekt-services, services]

View File

@ -650,9 +650,15 @@ rspec:merge-auto-explain-logs:
script:
- scripts/merge-auto-explain-logs
- |
if [[ -f "$RSPEC_AUTO_EXPLAIN_LOG_PATH" ]] && [[ "$CI_PROJECT_PATH" == "gitlab-org/gitlab" ]] && [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
source scripts/gitlab_component_helpers.sh
extract_and_upload_fingerprints
if [[ -f "$RSPEC_AUTO_EXPLAIN_LOG_PATH" ]] && [[ "$CI_PROJECT_PATH" == "gitlab-org/gitlab" ]]; then
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
# For the default branch (e.g., master), extract and upload fingerprints
source scripts/gitlab_component_helpers.sh
extract_and_upload_fingerprints
else
# For MRs, compare against master fingerprints and generate a report
scripts/merge_request_query_differ.rb "$RSPEC_AUTO_EXPLAIN_LOG_PATH"
fi
fi
artifacts:
name: auto-explain-logs
@ -1213,6 +1219,35 @@ rspec-ee system pg17:
# EE: default branch nightly scheduled jobs #
#####################################
##################################################
# EE: default branch weekly scheduled jobs #
# Integration tests with latest OpenSearch versions
# and the actual PG production version (PG16)
rspec-ee unit pg16 opensearch-latest:
extends:
- .rspec-base-pg16
- .rspec-ee-base-pg16-opensearch-latest
- .rspec-ee-unit-parallel
- .rails:rules:default-branch-schedule-weekly--code-backstage-ee-only
rspec-ee integration pg16 opensearch-latest:
extends:
- .rspec-base-pg16
- .rspec-ee-base-pg16-opensearch-latest
- .rspec-ee-integration-parallel
- .rails:rules:default-branch-schedule-weekly--code-backstage-ee-only
rspec-ee system pg16 opensearch-latest:
extends:
- .rspec-base-pg16
- .rspec-ee-base-pg16-opensearch-latest
- .rspec-ee-system-parallel
- .rails:rules:default-branch-schedule-weekly--code-backstage-ee-only
# EE: default branch weekly scheduled jobs #
##################################################
##################################################
# EE: Canonical MR pipelines
.rspec-fail-fast:

View File

@ -291,6 +291,11 @@ include:
- .use-pg16-es8-ee
- .rails:rules:run-search-tests
.rspec-ee-base-pg16-opensearch-latest:
extends:
- .rspec-base
- .use-pg16-opensearch-latest-ee
.rspec-ee-base-pg16-opensearch1:
extends:
- .rspec-base

View File

@ -192,6 +192,9 @@
.if-default-branch-schedule-nightly: &if-default-branch-schedule-nightly
if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "nightly"'
.if-default-branch-schedule-weekly: &if-default-branch-schedule-weekly
if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "weekly"'
.if-ruby-branch-schedule-nightly: &if-ruby-branch-schedule-nightly
if: '$CI_COMMIT_BRANCH == "ruby-next" && $CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TYPE == "nightly"'
@ -851,6 +854,8 @@
rules:
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
@ -1081,6 +1086,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-ruby-branch
- <<: *if-rails-next-branch
- <<: *if-force-ci
@ -1123,6 +1130,8 @@
- !reference [".qa:rules:e2e-test-never-run", rules]
- <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
when: never
- <<: *if-default-branch-schedule-weekly # already executed in the 2-hourly schedule
when: never
- <<: *if-default-branch-refs
- <<: *if-merge-request-labels-run-all-e2e
- <<: *if-merge-request-labels-run-cs-evaluation
@ -1228,6 +1237,8 @@
######################
.ci-templates:rules:shellcheck:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *ci-templates-patterns
- <<: *if-default-refs
@ -1252,6 +1263,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *code-backstage-patterns
@ -1272,6 +1285,8 @@
.docs:rules:docs-lint:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *docs-patterns
@ -1282,6 +1297,8 @@
.docs:rules:deprecations-and-removals:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *docs-deprecations-and-removals-patterns
@ -1296,6 +1313,8 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *code-backstage-qa-patterns
@ -1306,6 +1325,8 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *code-rest-api-patterns
@ -1316,6 +1337,8 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *audit-events-patterns
@ -1326,6 +1349,8 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *custom-roles-patterns
@ -1336,6 +1361,8 @@
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *ci-job-token-policies-patterns
@ -1386,6 +1413,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_COMPILE_PRODUCTION_ASSETS == "true"'
- <<: *if-tag
- <<: *if-sync-changes-on-stable-branches
@ -1428,6 +1457,8 @@
rules:
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_JEST_INTEGRATION == "true"'
- if: '$ENABLE_RSPEC_FRONTEND_FIXTURE == "true"'
- if: '$ENABLE_ESLINT == "true"'
@ -1458,6 +1489,8 @@
# From .frontend:rules:default-frontend-jobs
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-fork-merge-request
changes: *code-backstage-patterns
- if: '$ENABLE_RSPEC_FRONTEND_FIXTURE == "true"'
@ -1488,6 +1521,8 @@
# .frontend:rules:default-frontend-jobs, with a additional rules when MR is not approved
.frontend:rules:compile-storybook:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
# From .frontend:rules:default-frontend-jobs
- <<: *if-merge-request-labels-pipeline-expedite
when: never
@ -1512,6 +1547,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-dot-com-gitlab-org-default-branch
changes: *code-backstage-patterns
- <<: *if-merge-request-not-approved
@ -1529,6 +1566,8 @@
when: never
- <<: *if-fork-merge-request
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_JEST == "true"'
- <<: *if-automated-merge-request
changes: *code-backstage-patterns
@ -1568,6 +1607,8 @@
# From .frontend:rules:default-frontend-jobs
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_JEST_INTEGRATION == "true"'
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request-labels-frontend-and-feature-flag
@ -1588,6 +1629,8 @@
when: never
- <<: *if-merge-request
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-branch-refs
changes: *code-backstage-patterns
@ -1597,6 +1640,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-branch-refs
changes: *frontend-build-patterns
allow_failure: true
@ -1635,6 +1680,8 @@
.frontend:rules:jest-linters:
rules:
- <<: *if-merge-request-labels-run-all-jest
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: ["tooling/eslint-config/**/*", "spec/tooling/frontend/eslint-config/**/*"]
@ -1681,6 +1728,8 @@
############
.qa:rules:update-gem-cache:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-train
when: never
- if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\//'
@ -1700,6 +1749,8 @@
.qa:rules:update-orchestrator-gem-cache:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-train
when: never
- if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\//'
@ -1713,6 +1764,8 @@
.qa:rules:metadata-lint:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *qa-patterns
- <<: *if-default-refs
@ -1720,6 +1773,8 @@
.qa:rules:gitlab-orchestrator:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request
changes: *gitlab-orchestrator-patterns
- <<: *if-merge-request
@ -1752,12 +1807,16 @@
.qa:rules:internal:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_QA_INTERNAL == "true"'
- <<: *if-default-refs
changes: *qa-patterns
.qa:rules:selectors:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_QA_SELECTORS == "true"'
- <<: *if-default-refs
changes: *code-qa-patterns
@ -1768,6 +1827,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- *if-ruby-branch
- *if-rails-next-branch
- *if-merge-request-labels-run-all-e2e
@ -1805,6 +1866,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
# primary triggers for e2e tests
.qa:rules:e2e-blocking-base-before:
@ -1954,6 +2017,8 @@
when: never
- <<: *if-default-branch-schedule-nightly # already executed in the 2-hourly schedule
when: never
- <<: *if-default-branch-schedule-weekly # already executed in the 2-hourly schedule
when: never
- !reference [".qa:rules:e2e-blocking-base-before", rules]
# Run tests automatically for MRs that touch Ruby version files
- <<: *if-merge-request
@ -2135,6 +2200,8 @@
rules:
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-default-refs
changes: *db-backup-patterns
@ -2176,6 +2243,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- if: '$ENABLE_RSPEC_MIGRATION == "true"'
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
@ -2354,6 +2423,8 @@
.rails:rules:ee-and-foss-fast_spec_helper:
rules:
- if: '$ENABLE_RSPEC_FAST_SPEC_HELPER == "true"'
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *backend-patterns
@ -2374,6 +2445,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-run-all-rspec
- <<: *if-merge-request
changes: *core-backend-patterns
@ -2553,10 +2626,12 @@
.rails:rules:default-branch-schedule-nightly--code-backstage-default-rules:
rules:
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-nightly
.rails:rules:default-branch-schedule-weekly--code-backstage-default-rules:
rules:
- <<: *if-default-branch-schedule-weekly
.rails:rules:default-branch-schedule-nightly--code-backstage:
rules:
- !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules]
@ -2567,10 +2642,18 @@
when: never
- !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules]
.rails:rules:default-branch-schedule-weekly--code-backstage-ee-only:
rules:
- <<: *if-not-ee
when: never
- !reference [".rails:rules:default-branch-schedule-weekly--code-backstage-default-rules", rules]
.rails:rules:rspec-feature-flags:
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-refs
@ -2584,6 +2667,8 @@
when: never
- if: '$FAST_QUARANTINE == "false" && $RETRY_FAILED_TESTS_IN_NEW_PROCESS != "true"'
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-branch-refs
changes: *code-backstage-patterns
when: always
@ -2600,6 +2685,8 @@
.static-analysis:rules:static-analysis:
rules:
- if: '$ENABLE_STATIC_ANALYSIS == "true"'
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *code-backstage-qa-patterns
- <<: *if-default-refs
@ -2607,12 +2694,16 @@
.static-analysis:rules:static-verification-with-database:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *code-backstage-qa-patterns
.static-analysis:rules:rubocop:
rules:
- if: '$ENABLE_RUBOCOP == "true"'
- <<: *if-default-branch-schedule-weekly
when: never
# Do not run full rubocop if the merge request isn't approved
- <<: *if-merge-request-not-approved
changes: *rubocop-patterns
@ -2627,6 +2718,8 @@
.static-analysis:rules:ensure-application-settings-have-definition-file:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes:
- db/structure.sql
@ -2634,6 +2727,8 @@
.static-analysis:rules:haml-lint:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *rubocop-patterns
- <<: *if-default-refs
@ -2704,6 +2799,8 @@
#################
.reports:rules:code_quality:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- if: '$CODE_QUALITY_DISABLED'
@ -2723,6 +2820,8 @@
.reports:rules:semgrep-sast:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- if: $SAST_DISABLED
@ -2742,6 +2841,8 @@
.reports:rules:secret_detection:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- if: '$SECRET_DETECTION_DISABLED'
@ -2753,6 +2854,8 @@
.reports:rules:gemnasium-dependency_scanning:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /gemnasium([^-]|$)/'
@ -2764,6 +2867,8 @@
.reports:rules:gemnasium-python-dependency_scanning:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/ || $DS_EXCLUDED_ANALYZERS =~ /gemnasium-python/'
@ -2805,6 +2910,8 @@
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-merge-request
changes:
- '{,ee/}app/**/*.{js,vue,rb,haml}'
@ -2980,6 +3087,8 @@
rules:
- <<: *if-merge-request-labels-pipeline-expedite
when: never
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *code-backstage-patterns
@ -3110,6 +3219,8 @@
###################
.workhorse:rules:workhorse:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *workhorse-patterns
@ -3118,16 +3229,22 @@
###################
.yaml-lint:rules:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *yaml-lint-patterns
.lint-pipeline-yaml:rules:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *lint-pipeline-yaml-patterns
.lint-metrics-yaml:rules:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *lint-metrics-yaml-patterns
@ -3429,6 +3546,8 @@
###################
.benchmark:rules:benchmark-markdown:
rules:
- <<: *if-default-branch-schedule-weekly
when: never
- <<: *if-default-refs
changes: *setup-test-env-patterns
when: manual

View File

@ -313,7 +313,6 @@ RSpec/BeEq:
- 'ee/spec/requests/api/graphql/audit_events/streaming/http/namespace_filters/create_spec.rb'
- 'ee/spec/requests/api/graphql/audit_events/streaming/instance_headers/create_spec.rb'
- 'ee/spec/requests/api/graphql/boards/boards_query_spec.rb'
- 'ee/spec/requests/api/graphql/gitlab_subscriptions/add_on_purchase_spec.rb'
- 'ee/spec/requests/api/graphql/gitlab_subscriptions/namespaces/add_on_purchase_spec.rb'
- 'ee/spec/requests/api/graphql/group/ci_cd_settings_spec.rb'
- 'ee/spec/requests/api/graphql/group/dast_profile_schedule_spec.rb'

View File

@ -1701,7 +1701,6 @@ RSpec/FeatureCategory:
- 'spec/helpers/export_helper_spec.rb'
- 'spec/helpers/external_link_helper_spec.rb'
- 'spec/helpers/feature_flags_helper_spec.rb'
- 'spec/helpers/git_helper_spec.rb'
- 'spec/helpers/gitlab_routing_helper_spec.rb'
- 'spec/helpers/gitlab_script_tag_helper_spec.rb'
- 'spec/helpers/graph_helper_spec.rb'

View File

@ -59,9 +59,7 @@ export default {
}));
},
error(error) {
createAlert({
message: s__('Pipelines|There was a problem fetching the pipeline inputs.'),
});
this.createErrorAlert(error);
reportToSentry(this.$options.name, error);
},
},
@ -98,6 +96,14 @@ export default {
this.$emit('update-inputs', nameValuePairs);
},
createErrorAlert(error) {
const graphQLErrors = error?.graphQLErrors?.map((err) => err.message) || [];
const message = graphQLErrors.length
? graphQLErrors.join(', ')
: s__('Pipelines|There was a problem fetching the pipeline inputs. Please try again.');
createAlert({ message });
},
},
};
</script>

View File

@ -17,6 +17,7 @@ import CandidateList from '~/ml/model_registry/components/candidate_list.vue';
import DeleteModelDisclosureDropdownItem from '../components/delete_model_disclosure_dropdown_item.vue';
import LoadOrErrorOrShow from '../components/load_or_error_or_show.vue';
import DeleteModel from '../components/functional/delete_model.vue';
import SidebarItem from '../components/model_sidebar_item.vue';
const ROUTE_DETAILS = 'details';
const ROUTE_VERSIONS = 'versions';
@ -64,6 +65,7 @@ export default {
GlSprintf,
GlIcon,
GlLink,
SidebarItem,
},
mixins: [timeagoMixin],
router: new VueRouter({
@ -290,7 +292,7 @@ export default {
<div class="gl-grid gl-gap-3 md:gl-grid-cols-4">
<div class="gl-pr-8 md:gl-col-span-3">
<load-or-error-or-show :is-loading="isLoading" :error-message="errorMessage">
<gl-tabs class="gl-mt-4" :value="tabIndex">
<gl-tabs :value="tabIndex">
<gl-tab
:title="$options.i18n.tabModelCardTitle"
@click="goTo($options.ROUTE_DETAILS)"
@ -315,10 +317,9 @@ export default {
</load-or-error-or-show>
</div>
<div class="gl-pt-6 md:gl-col-span-1">
<div>
<div class="gl-text-lg gl-font-bold">{{ $options.i18n.authorTitle }}</div>
<div class="gl-pt-2 gl-text-subtle" data-testid="sidebar-author">
<div class="gl-flex gl-flex-col gl-gap-5 md:gl-col-span-1">
<sidebar-item :title="$options.i18n.authorTitle" class="gl-border-t-0">
<div class="gl-mt-2" data-testid="sidebar-author">
<gl-link
v-if="showModelAuthor"
data-testid="sidebar-author-link"
@ -330,12 +331,14 @@ export default {
</gl-link>
<span v-else>{{ $options.i18n.noneText }}</span>
</div>
</div>
<div v-if="showDefaultExperiment" class="gl-mt-5">
<div class="gl-text-lg gl-font-bold" data-testid="sidebar-experiment-title">
{{ $options.i18n.experimentTitle }}
</div>
<div class="gl-pt-2 gl-text-subtle" data-testid="sidebar-experiment-label">
</sidebar-item>
<sidebar-item
v-if="showDefaultExperiment"
:title="$options.i18n.experimentTitle"
data-testid="sidebar-experiment"
>
<div data-testid="sidebar-experiment-label">
<gl-link
data-testid="sidebar-latest-experiment-link"
:href="model.defaultExperimentPath"
@ -343,10 +346,10 @@ export default {
{{ $options.i18n.defaultExperimentPath }}
</gl-link>
</div>
</div>
<div class="gl-mt-5">
<div class="gl-text-lg gl-font-bold">{{ $options.i18n.latestVersionTitle }}</div>
<div class="gl-pt-2 gl-text-subtle" data-testid="sidebar-latest-version">
</sidebar-item>
<sidebar-item :title="$options.i18n.latestVersionTitle">
<div data-testid="sidebar-latest-version">
<gl-link
v-if="showModelLatestVersion"
data-testid="sidebar-latest-version-link"
@ -356,16 +359,16 @@ export default {
</gl-link>
<span v-else>{{ $options.i18n.noneText }}</span>
</div>
</div>
<div class="gl-mt-5">
<div class="gl-text-lg gl-font-bold">{{ $options.i18n.versionCountTitle }}</div>
<div class="gl-pt-2 gl-text-subtle" data-testid="sidebar-version-count">
</sidebar-item>
<sidebar-item :title="$options.i18n.versionCountTitle">
<div data-testid="sidebar-version-count">
<span v-if="versionCount">
{{ versionCount }}
</span>
<span v-else>{{ $options.i18n.noneText }}</span>
</div>
</div>
</sidebar-item>
</div>
</div>
</div>

View File

@ -60,16 +60,14 @@ export default {
<template>
<div class="issue-details issuable-details">
<div
v-if="model.descriptionHtml"
class="detail-page-description js-detail-page-description gl-pt-4"
>
<div v-if="model.descriptionHtml" class="detail-page-description js-detail-page-description">
<issuable-description
:issuable="issuable"
:enable-task-list="enableTaskList"
:can-edit="canEditRequirement"
:data-update-url="dataUpdateUrl"
:task-list-update-path="taskListUpdatePath"
class="gl-leading-20"
/>
</div>
<div v-else class="gl-text-subtle" data-testid="empty-description-state">

View File

@ -0,0 +1,22 @@
<script>
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div class="gl-border-t gl-flex gl-flex-col gl-gap-2 gl-border-t-subtle gl-pt-5">
<h3 class="gl-heading-5 gl-mb-0">
{{ title }}
</h3>
<div class="gl-text-subtle">
<slot></slot>
</div>
</div>
</template>

View File

@ -1,13 +1,18 @@
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { GlFormGroup, GlFormInput, GlFormSelect, GlSprintf, GlLink } from '@gitlab/ui';
import { kebabCase } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
import validation, { initForm } from '~/vue_shared/directives/validation';
import { K8S_OPTION, DEPLOYMENT_TARGET_SELECTIONS } from '../form_constants';
import NewProjectDestinationSelect from './project_destination_select.vue';
export default {
components: {
GlFormGroup,
GlFormInput,
GlFormSelect,
GlSprintf,
GlLink,
NewProjectDestinationSelect,
},
directives: {
@ -29,8 +34,14 @@ export default {
return {
form,
selectedNamespace: this.namespace,
selectedTarget: null,
};
},
computed: {
isK8sOptionSelected() {
return this.selectedTarget === K8S_OPTION.value;
},
},
methods: {
updateSlug() {
this.form.fields['project[path]'].value = kebabCase(this.form.fields['project[name]'].value);
@ -39,6 +50,9 @@ export default {
this.$emit('onSelectNamespace', newNamespace);
},
},
helpPageK8s: helpPagePath('user/clusters/agent/_index'),
K8S_OPTION,
DEPLOYMENT_TARGET_SELECTIONS,
};
</script>
@ -71,17 +85,21 @@ export default {
<div class="gl-flex gl-flex-col gl-gap-4 sm:gl-flex-row">
<gl-form-group
:label="s__('ProjectsNew|Choose a group or namespace')"
class="sm:gl-w-1/2"
label-for="namespace"
:invalid-feedback="
s__('ProjectsNew|Pick a group or namespace where you want to create this project.')
"
:state="selectedNamespace.id !== null"
data-testid="project-namespace-group"
>
<template #label>
<label id="namespace-selector" for="namespace" class="gl-mb-0">
{{ s__('ProjectsNew|Choose a group or namespace') }}
</label>
</template>
<new-project-destination-select
toggle-aria-labelled-by="namespace"
toggle-aria-labelled-by="namespace-selector"
toggle-id="namespace"
:namespace-id="selectedNamespace.id"
:namespace-full-path="selectedNamespace.fullPath"
@onSelectNamespace="onSelectNamespace"
@ -111,6 +129,45 @@ export default {
</gl-form-group>
</div>
<!-- Deployment Target and Visibility Level should be added in: https://gitlab.com/gitlab-org/gitlab/-/issues/514700 -->
<gl-form-group
:label="s__('Deployment Target|Project deployment target (optional)')"
label-for="deployment-target-select"
data-testid="deployment-target-form-group"
>
<gl-form-select
id="deployment-target-select"
v-model="selectedTarget"
:options="$options.DEPLOYMENT_TARGET_SELECTIONS"
class="gl-w-full"
data-testid="deployment-target-select"
>
<template #first>
<option :value="null" disabled>
{{ s__('Deployment Target|Select the deployment target') }}
</option>
</template>
</gl-form-select>
<template v-if="isK8sOptionSelected" #description>
<gl-sprintf
:message="
s__(
'Deployment Target|%{linkStart}How to provision or deploy to Kubernetes clusters from GitLab?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link
:href="$options.helpPageK8s"
data-track-action="visit_docs"
data-track-label="new_project_deployment_target"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</template>
</gl-form-group>
<!-- Visibility Level should be added in: https://gitlab.com/gitlab-org/gitlab/-/issues/514700 -->
</div>
</template>

View File

@ -0,0 +1,64 @@
import { s__ } from '~/locale';
export const K8S_OPTION = {
value: 'kubernetes',
text: s__('DeploymentTarget|Kubernetes (GKE, EKS, OpenShift, and so on)'),
};
export const DEPLOYMENT_TARGET_SELECTIONS = [
K8S_OPTION,
{
value: 'managed_container_runtime',
text: s__('DeploymentTarget|Managed container runtime (Fargate, Cloud Run, DigitalOcean App)'),
},
{
value: 'self_managed_container_runtime',
text: s__(
'DeploymentTarget|Self-managed container runtime (Podman, Docker Swarm, Docker Compose)',
),
},
{
value: 'heroku',
text: s__('DeploymentTarget|Heroku'),
},
{
value: 'virtual_machine',
text: s__('DeploymentTarget|Virtual machine (for example, EC2)'),
},
{
value: 'mobile_app_store',
text: s__('DeploymentTarget|Mobile app store'),
},
{
value: 'registry',
text: s__('DeploymentTarget|Registry (package or container)'),
},
{
value: 'infrastructure_provider',
text: s__('DeploymentTarget|Infrastructure provider (Terraform, Cloudformation, and so on)'),
},
{
value: 'serverless_backend',
text: s__('DeploymentTarget|Serverless backend (Lambda, Cloud functions)'),
},
{
value: 'edge_computing',
text: s__('DeploymentTarget|Edge Computing (e.g. Cloudflare Workers)'),
},
{
value: 'web_deployment_platform',
text: s__('DeploymentTarget|Web Deployment Platform (Netlify, Vercel, Gatsby)'),
},
{
value: 'gitlab_pages',
text: s__('DeploymentTarget|GitLab Pages'),
},
{
value: 'other_hosting_service',
text: s__('DeploymentTarget|Other hosting service'),
},
{
value: 'no_deployment',
text: s__('DeploymentTarget|No deployment planned'),
},
];

View File

@ -126,7 +126,9 @@ export default {
</script>
<template>
<div class="row-content-block gl-flex gl-flex-col gl-gap-3 md:gl-flex-row">
<div
class="row-content-block !-gl-mt-3 gl-flex gl-flex-col gl-gap-3 gl-border-y-0 md:gl-flex-row"
>
<gl-filtered-search
v-model="internalFilter"
class="gl-min-w-0 gl-grow"

View File

@ -5,6 +5,7 @@ module API
class GraphqlExplorerController < BaseActionController
include Gitlab::GonHelper
include WithPerformanceBar
include ViteCSP
def show
# We need gon to setup gon.relative_url_root which is used by our Apollo client

View File

@ -273,13 +273,13 @@ class MergeRequestsFinder < IssuableFinder
return items unless params.review_state.present?
return items if params.reviewer_id? || params.reviewer_username?
items.review_states(params.review_state)
items.review_states(params.review_state, params.ignored_reviewer)
end
def by_negated_review_states(items)
return items unless params.not_review_states.present?
items.no_review_states(params.not_review_states)
items.no_review_states(params.not_review_states, params.ignored_reviewer)
end
def by_negated_reviewer(items)

View File

@ -20,6 +20,13 @@ class MergeRequestsFinder
end
end
def ignored_reviewer
return unless params[:ignored_reviewer_username].present?
User.find_by_username(params[:ignored_reviewer_username])
end
strong_memoize_attr :ignored_reviewer
def assigned_user
strong_memoize(:assigned_user) do
next unless params[:assigned_user_id].present?

View File

@ -126,6 +126,10 @@ module Resolvers
required: false,
description: 'Merge requests updated before the timestamp.'
argument :ignored_reviewer_username, GraphQL::Types::String,
required: false,
description: 'Username of the reviewer to ignore when searching by reviewer state.',
experiment: { milestone: '18.0' }
argument :label_name, [GraphQL::Types::String, { null: true }],
required: false,
description: 'Labels applied to the merge request.'

View File

@ -4,6 +4,7 @@ module GitHelper
def strip_signature(text)
text = text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
text = text.gsub(/-----BEGIN PGP MESSAGE-----(.*)-----END PGP MESSAGE-----/m, "")
text = text.gsub(/-----BEGIN SSH SIGNATURE-----(.*)-----END SSH SIGNATURE-----/m, "")
text.gsub(/-----BEGIN SIGNED MESSAGE-----(.*)-----END SIGNED MESSAGE-----/m, "")
end

View File

@ -518,12 +518,13 @@ class MergeRequest < ApplicationRecord
)
end
scope :review_states, ->(states) do
where(
reviewers_subquery
.where(Arel::Table.new("#{to_ability_name}_reviewers")[:state].in(states))
.exists
)
scope :review_states, ->(states, ignored_reviewer = nil) do
reviewers = Arel::Table.new("#{to_ability_name}_reviewers")
scope = reviewers_subquery.where(reviewers[:state].in(states))
scope = scope.where(reviewers[:user_id].not_eq(ignored_reviewer.id)) if ignored_reviewer
where(scope.exists)
end
scope :not_only_reviewer, ->(user) do
@ -539,16 +540,15 @@ class MergeRequest < ApplicationRecord
review_requested.where.not(subquery.exists)
end
scope :no_review_states, ->(states) do
where(
reviewers_subquery.exists
)
.where(
reviewers_subquery
.where(Arel::Table.new("#{to_ability_name}_reviewers")[:state].in(states))
.exists
.not
)
scope :no_review_states, ->(states, ignored_reviewer = nil) do
reviewers = Arel::Table.new("#{to_ability_name}_reviewers")
scope = reviewers_subquery
scope = scope.where(reviewers[:user_id].not_eq(ignored_reviewer.id)) if ignored_reviewer
forbidden = scope.clone.where(reviewers[:state].in(states))
where(scope.exists).where(forbidden.exists.not)
end
scope :assignee_or_reviewer, ->(user, assigned_review_states, reviewer_state) do

View File

@ -17,11 +17,11 @@ module Ci
def execute
unless current_user.can?(:download_code, project)
return error_response('insufficient permissions to read inputs')
return error_response(s_('Pipelines|Insufficient permissions to read inputs'))
end
if !project.repository.branch_or_tag?(ref) || sha.blank?
return error_response('ref can only be an existing branch or tag')
return error_response(s_('Pipelines|Can only run new pipelines for an existing branch or tag'))
end
# The project config may not exist if the project is using a policy.
@ -36,14 +36,14 @@ module Ci
# We need to read the uninterpolated YAML of the included file.
yaml_content = ::Gitlab::Ci::Config::Yaml.load!(project_config.content)
yaml_result = yaml_result_of_internal_include(yaml_content)
return error_response('invalid YAML config') unless yaml_result&.valid?
return error_response(s_('Pipelines|Invalid YAML syntax')) unless yaml_result&.valid?
spec_inputs = Ci::PipelineCreation::Inputs::SpecInputs.new(yaml_result.spec[:inputs])
return error_response(spec_inputs.errors.join(', ')) if spec_inputs.errors.any?
success_response(spec_inputs)
else
error_response('inputs not supported for this CI config source')
error_response(s_('Pipelines|Inputs not supported for this CI config source'))
end
rescue ::Gitlab::Ci::Config::Yaml::LoadError => e
error_response("YAML load error: #{e.message}")

View File

@ -1,9 +0,0 @@
---
name: group_remove_dormant_members
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/461339
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/153118
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/461567
milestone: '17.1'
group: group::utilization
type: wip
default_enabled: false

View File

@ -0,0 +1,34 @@
const replacer = (fullImport, importName, importPath) => {
const workerUrl = `${importName}Url`;
const blobName = `${importName}Blob`;
return `/* worker import was replaced to support cross-origin workers */
import ${workerUrl} from '${importPath}&url';
const ${blobName} = new Blob([\`import \${JSON.stringify(new URL(${workerUrl}, import.meta.url))}\`], { type: "application/javascript" });
function ${importName}(options) {
const objURL = URL.createObjectURL(${blobName});
const worker = new Worker(objURL, { type: "module", name: options?.name });
worker.addEventListener("error", (e) => { URL.revokeObjectURL(objURL) });
return worker;
}
/* end of replaced code */`;
};
export const CrossOriginWorkerPlugin = () => {
let config;
return {
name: 'vite-worker-transform-plugin',
configResolved(resolvedConfig) {
config = resolvedConfig;
},
transform(code) {
if (config.command !== 'serve' || !code.includes('?worker')) {
return null;
}
return {
code: code.replace(/import\s+(\w+)\s+from\s+['"](.*?\?worker)['"];/g, replacer),
map: null,
};
},
};
};

View File

@ -132,11 +132,6 @@ Settings = GitlabSettings.load(file, Rails.env) do
File.expand_path(path, Rails.root)
end
# FIXME: Deprecated in favor of Gitlab::Encryption::KeyProvider
def attr_encrypted_db_key_base_truncated
db_key_base_keys_truncated.first
end
# Don't use this in new code, use db_key_base_keys_32_bytes instead!
def db_key_base_keys_truncated
db_key_base_keys.map do |key| # rubocop:disable Rails/Pluck -- No Rails context
@ -144,11 +139,6 @@ Settings = GitlabSettings.load(file, Rails.env) do
end
end
# FIXME: Deprecated in favor of Gitlab::Encryption::KeyProvider
def attr_encrypted_db_key_base_32
db_key_base_keys_32_bytes.first
end
# Ruby 2.4+ requires passing in the exact required length for OpenSSL keys
# (https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1).
# Previous versions quietly truncated the input.
@ -162,11 +152,6 @@ Settings = GitlabSettings.load(file, Rails.env) do
end
end
# FIXME: Deprecated in favor of Gitlab::Encryption::KeyProvider
def attr_encrypted_db_key_base
db_key_base_keys.first
end
# This should be used for :per_attribute_iv_and_salt mode. There is no
# need to truncate the key because the encryptor will use the salt to
# generate a hash of the password:

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class PrepareToDropIndexPCiBuildsTriggerRequestId < Gitlab::Database::Migration[2.2]
milestone '18.0'
disable_ddl_transaction!
TABLE = :p_ci_builds
COLUMN = :trigger_request_id
INDEX_NAME = :tmp_p_ci_builds_trigger_request_id_idx
def up
prepare_async_index_removal(TABLE, COLUMN, name: INDEX_NAME)
end
def down
unprepare_async_index(TABLE, COLUMN, name: INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
7be20294f7af1c1deb5e50b196ac9cfea96f7783c3bc9b4f90ea3a59f5477fa8

View File

@ -80,24 +80,6 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="queryabusereportlabelssearchterm"></a>`searchTerm` | [`String`](#string) | Search term to find labels with. |
### `Query.addOnPurchase`
Retrieve the active add-on purchase. This query can be used in GitLab SaaS and self-managed environments.
{{< details >}}
**Deprecated** in GitLab 17.4.
Use [addOnPurchases](#queryaddonpurchases) instead.
{{< /details >}}
Returns [`AddOnPurchase`](#addonpurchase).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryaddonpurchaseaddontype"></a>`addOnType` | [`GitlabSubscriptionsAddOnType!`](#gitlabsubscriptionsaddontype) | Type of add-on for the add-on purchase. |
| <a id="queryaddonpurchasenamespaceid"></a>`namespaceId` | [`NamespaceID`](#namespaceid) | ID of namespace that the add-on was purchased for. |
### `Query.addOnPurchases`
Retrieve all active add-on purchases. This query can be used in GitLab.com and self-managed environments.
@ -20495,6 +20477,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="addonuserassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="addonuserassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="addonuserassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="addonuserassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="addonuserassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="addonuserassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="addonuserassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -20553,6 +20536,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="addonuserauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="addonuserauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="addonuserauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="addonuserauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="addonuserauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="addonuserauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="addonuserauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -20673,6 +20657,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="addonuserreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="addonuserreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="addonuserreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="addonuserreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="addonuserreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="addonuserreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="addonuserreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -21536,6 +21521,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="autocompleteduserassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="autocompleteduserassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="autocompleteduserassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="autocompleteduserassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="autocompleteduserassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="autocompleteduserassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="autocompleteduserassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -21594,6 +21580,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="autocompleteduserauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="autocompleteduserauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="autocompleteduserauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="autocompleteduserauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="autocompleteduserauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="autocompleteduserauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="autocompleteduserauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -21726,6 +21713,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="autocompleteduserreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="autocompleteduserreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="autocompleteduserreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="autocompleteduserreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="autocompleteduserreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="autocompleteduserreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="autocompleteduserreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -24383,6 +24371,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="currentuserassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="currentuserassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="currentuserassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="currentuserassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="currentuserassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="currentuserassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="currentuserassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -24444,6 +24433,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="currentuserassigneeorreviewermergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="currentuserassigneeorreviewermergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="currentuserassigneeorreviewermergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="currentuserassigneeorreviewermergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="currentuserassigneeorreviewermergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="currentuserassigneeorreviewermergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="currentuserassigneeorreviewermergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -24501,6 +24491,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="currentuserauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="currentuserauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="currentuserauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="currentuserauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="currentuserauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="currentuserauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="currentuserauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -24621,6 +24612,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="currentuserreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="currentuserreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="currentuserreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="currentuserreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="currentuserreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="currentuserreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="currentuserreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -28357,6 +28349,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="groupmergerequestsdeploymentid"></a>`deploymentId` | [`String`](#string) | ID of the deployment. |
| <a id="groupmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="groupmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="groupmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="groupmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="groupmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="groupmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Return merge requests from archived projects. |
@ -30833,6 +30826,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestassigneeassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestassigneeassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestassigneeassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestassigneeassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestassigneeassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestassigneeassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestassigneeassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -30891,6 +30885,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestassigneeauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestassigneeauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestassigneeauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestassigneeauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestassigneeauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestassigneeauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestassigneeauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31011,6 +31006,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestassigneereviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestassigneereviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestassigneereviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestassigneereviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestassigneereviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestassigneereviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestassigneereviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31252,6 +31248,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestauthorassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestauthorassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestauthorassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestauthorassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestauthorassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestauthorassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestauthorassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31310,6 +31307,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestauthorauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestauthorauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestauthorauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestauthorauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestauthorauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestauthorauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestauthorauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31430,6 +31428,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestauthorreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestauthorreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestauthorreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestauthorreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestauthorreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestauthorreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestauthorreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31722,6 +31721,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestparticipantassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestparticipantassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestparticipantassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestparticipantassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestparticipantassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestparticipantassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestparticipantassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31780,6 +31780,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestparticipantauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestparticipantauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestparticipantauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestparticipantauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestparticipantauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestparticipantauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestparticipantauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -31900,6 +31901,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestparticipantreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -32160,6 +32162,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestreviewerassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestreviewerassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestreviewerassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestreviewerassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestreviewerassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestreviewerassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestreviewerassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -32218,6 +32221,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestreviewerauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestreviewerauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestreviewerauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestreviewerauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestreviewerauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestreviewerauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestreviewerauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -32338,6 +32342,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="mergerequestreviewerreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -36069,6 +36074,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="projectmergerequestsdeploymentid"></a>`deploymentId` | [`String`](#string) | ID of the deployment. |
| <a id="projectmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="projectmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="projectmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="projectmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="projectmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="projectmergerequestslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to the merge request. |
@ -39606,6 +39612,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="usercoreassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="usercoreassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="usercoreassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="usercoreassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="usercoreassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="usercoreassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="usercoreassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -39664,6 +39671,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="usercoreauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="usercoreauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="usercoreauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="usercoreauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="usercoreauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="usercoreauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="usercoreauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -39784,6 +39792,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="usercorereviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="usercorereviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="usercorereviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="usercorereviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="usercorereviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="usercorereviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="usercorereviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -48064,6 +48073,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="userassignedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="userassignedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="userassignedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="userassignedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="userassignedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="userassignedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="userassignedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -48122,6 +48132,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="userauthoredmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="userauthoredmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="userauthoredmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="userauthoredmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="userauthoredmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="userauthoredmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="userauthoredmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |
@ -48242,6 +48253,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="userreviewrequestedmergerequestsdraft"></a>`draft` | [`Boolean`](#boolean) | Limit result to draft merge requests. |
| <a id="userreviewrequestedmergerequestsenvironmentname"></a>`environmentName` | [`String`](#string) | Environment merge requests have been deployed to. |
| <a id="userreviewrequestedmergerequestsgroupid"></a>`groupId` | [`GroupID`](#groupid) | The global ID of the group the authored merge requests should be in. Merge requests in subgroups are included. |
| <a id="userreviewrequestedmergerequestsignoredreviewerusername"></a>`ignoredReviewerUsername` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.0. **Status**: Experiment. Username of the reviewer to ignore when searching by reviewer state. |
| <a id="userreviewrequestedmergerequestsiids"></a>`iids` | [`[String!]`](#string) | Array of IIDs of merge requests, for example `[1, 2]`. |
| <a id="userreviewrequestedmergerequestsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'. |
| <a id="userreviewrequestedmergerequestsincludearchived"></a>`includeArchived` | [`Boolean`](#boolean) | Merge requests from archived projects. |

View File

@ -217,7 +217,7 @@ namespace_object.recursive_self_and_hierarchy
### Search using trie data structure
`Namespaces::Traversal::TrieNode` implements a trie data structure to efficiently search within
`namespaces.traveral_ids` hierarchy for a set of Namespaces.
`namespaces.traversal_ids` hierarchy for a set of Namespaces.
```ruby
traversal_ids = [[9970, 123], [9970, 456]] # Derived from (for example): Namespace.where(...).map(&:traversal_ids)

View File

@ -755,6 +755,7 @@ NOTE: With the addition of PG17, we are close to the limit of nightly jobs, with
| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour at XX:05) | 16 (default version) | 3.2 (default version) |
| `maintenance` scheduled pipelines for the `ruby-next` branch (every odd-numbered hour at XX:10) | 16 (default version) | 3.3 |
| `nightly` scheduled pipelines for the `master` branch | 16 (default version), 14, 15 and 17 | 3.2 (default version) |
| `weekly` scheduled pipelines for the `master` branch | 16 (default version) | 3.2 (default version) |
For the next Ruby versions we're testing against with, we run
maintenance scheduled pipelines every 2 hours on the `ruby-next` branch.
@ -811,6 +812,7 @@ test suites use PostgreSQL 16 because there is no dependency between the databas
|-------------------------------------------------------------------------------------------------|-----------------------|----------------------|----------------------|
| Merge requests with label `~group::global search` or `~pipeline:run-search-tests` | 8.X (production) | | 16 (default version) |
| `nightly` scheduled pipelines for the `master` branch | 7.X, 8.X (production) | 1.X, 2.X | 16 (default version) |
| `weekly` scheduled pipelines for the `master` branch | | latest | 16 (default version) |
## Monitoring

View File

@ -1411,4 +1411,7 @@ exists to create this tutorial.
- <i class="fa-youtube-play" aria-hidden="true"></i>
[GitLab Duo Code Suggestions](https://youtu.be/ds7SG1wgcVM?si=MfbzPIDpikGhoPh7)
<!-- Video published on 2024-01-24 -->
- <i class="fa-youtube-play" aria-hidden="true"></i>
[Application modernization with GitLab Duo (C++ to Java)](https://youtu.be/FjoAmt5eeXA?si=SLv9Mv8eSUAVwW5Z)
<!-- Video published on 2025-03-18 -->
<!-- markdownlint-enable -->

View File

@ -249,7 +249,11 @@ You can also add additional instructions to be considered. For example:
- `/explain how concurrency works in this context` (Go)
- `/explain how the request reaches the client` (REST API, database)
For more information, see [Use GitLab Duo Chat in VS Code](_index.md#use-gitlab-duo-chat-in-vs-code).
For more information, see:
- [Use GitLab Duo Chat in VS Code](_index.md#use-gitlab-duo-chat-in-vs-code).
- <i class="fa-youtube-play" aria-hidden="true"></i> [Application modernization with GitLab Duo (C++ to Java)](https://youtu.be/FjoAmt5eeXA?si=SLv9Mv8eSUAVwW5Z).
<!-- Video published on 2025-03-18 -->
In the GitLab UI, you can also explain code in:
@ -420,6 +424,9 @@ You can include additional instructions to be considered. For example:
- Focus on performance, for example `/refactor improving performance`.
- Focus on potential vulnerabilities, for example `/refactor avoiding memory leaks and exploits`.
For more information, see <i class="fa-youtube-play" aria-hidden="true"></i> [Application modernization with GitLab Duo (C++ to Java)](https://youtu.be/FjoAmt5eeXA?si=SLv9Mv8eSUAVwW5Z).
<!-- Video published on 2025-03-18 -->
## Fix code in the IDE
{{< details >}}

View File

@ -85,14 +85,6 @@ To unban a user:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/461339) in GitLab 17.1 [with a flag](../../administration/feature_flags.md) named `group_remove_dormant_members`. Disabled by default.
> [Released](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178851) as a [beta](../../policy/development_stages_support.md#beta) feature in GitLab 17.9.
{{< alert type="flag" >}}
The availability of this feature is controlled by a feature flag.
For more information, see the history.
This feature is available for testing, but not ready for production use.
{{< /alert >}}
Prerequisites:
- You must have the Owner role for the group.

View File

@ -20,6 +20,8 @@ module Ci
@inputs = []
@errors = []
return unless valid_specs?(specs)
build_inputs!(specs.to_h)
end
@ -62,6 +64,17 @@ module Ci
@inputs << input_type.new(name: input_name, spec: spec)
end
end
def valid_specs?(specs)
return true if specs.respond_to?(:to_h)
@errors.push(
format(s_("Pipelines|Invalid input specification: expected a hash-like object, got %{class_name}"),
class_name: specs.class.name)
)
false
end
end
end
end

View File

@ -31,6 +31,7 @@ variables:
access: 'developer'
reports:
secret_detection: gl-secret-detection-report.json
cache: []
secret_detection:
extends: .secret-analyzer

View File

@ -40,6 +40,9 @@ module Gitlab
automatic_reindexing
end
# Temporary in order to truncate this table during low traffic
TruncateTaggings.new.execute
rescue StandardError => e
Gitlab::AppLogger.error(e)
raise

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Gitlab
module Database
class TruncateTaggings
include AsyncDdlExclusiveLeaseGuard
def execute
return unless Gitlab.com_except_jh? # rubocop:disable Gitlab/AvoidGitlabInstanceChecks -- it's not a feature
return unless taggings_has_any_data?
try_obtain_lease do
connection.execute('TRUNCATE TABLE "taggings"')
end
end
def taggings_has_any_data?
!!connection.select_value('SELECT TRUE FROM "taggings" LIMIT 1')
end
def connection
::Ci::ApplicationRecord.connection
end
def connection_db_config
::Ci::ApplicationRecord.connection_db_config
end
def lease_timeout
10.minutes
end
end
end
end

View File

@ -44702,6 +44702,9 @@ msgstr ""
msgid "Pipelines|By revoking a trigger token you will break any processes making use of it. Are you sure?"
msgstr ""
msgid "Pipelines|Can only run new pipelines for an existing branch or tag"
msgstr ""
msgid "Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})"
msgstr ""
@ -44777,6 +44780,18 @@ msgstr ""
msgid "Pipelines|Inputs"
msgstr ""
msgid "Pipelines|Inputs not supported for this CI config source"
msgstr ""
msgid "Pipelines|Insufficient permissions to read inputs"
msgstr ""
msgid "Pipelines|Invalid YAML syntax"
msgstr ""
msgid "Pipelines|Invalid input specification: expected a hash-like object, got %{class_name}"
msgstr ""
msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource."
msgstr ""
@ -44921,7 +44936,7 @@ msgstr ""
msgid "Pipelines|There was a problem fetching the pipeline iid."
msgstr ""
msgid "Pipelines|There was a problem fetching the pipeline inputs."
msgid "Pipelines|There was a problem fetching the pipeline inputs. Please try again."
msgstr ""
msgid "Pipelines|There was a problem fetching the pipeline stage jobs."
@ -59514,6 +59529,9 @@ msgstr ""
msgid "SuperSonics|Cannot activate instance due to a connectivity issue"
msgstr ""
msgid "SuperSonics|Could not activate subscription"
msgstr ""
msgid "SuperSonics|Customers Portal"
msgstr ""
@ -59556,6 +59574,9 @@ msgstr ""
msgid "SuperSonics|Start free trial"
msgstr ""
msgid "SuperSonics|Subscription cannot be activated if %{silentModeDocsLinkStart}Silent Mode%{silentModeDocsLinkEnd} is enabled. Disable Silent Mode and try again."
msgstr ""
msgid "SuperSonics|Subscription detail synchronization has started and will complete soon."
msgstr ""

View File

@ -1,47 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# Script to extract SQL query fingerprints from auto-explain logs
require 'json'
require 'zlib'
if ARGV.size < 2
puts "Usage: #{$PROGRAM_NAME} <input_file> <output_file>"
exit 1
end
input_file = ARGV[0]
output_file = ARGV[1]
unless File.exist?(input_file)
puts "Error: Input file not found - #{input_file}"
exit 1
end
fingerprints = Set.new
begin
# Handle both compressed and uncompressed files
if input_file.end_with?('.gz')
Zlib::GzipReader.open(input_file) do |gz|
gz.each_line do |line|
data = JSON.parse(line)
fingerprints.add(data['fingerprint']) if data['fingerprint']
rescue JSON::ParserError
# empty
end
end
else
File.foreach(input_file) do |line|
data = JSON.parse(line)
fingerprints.add(data['fingerprint']) if data['fingerprint']
rescue JSON::ParserError
# empty
end
end
File.open(output_file, 'w') { |f| fingerprints.each { |fp| f.puts(fp) } }
rescue StandardError => e
puts "Error: #{e.message}"
exit 1
end

View File

@ -232,7 +232,7 @@ export FINGERPRINTS_PACKAGE_URL="${API_PACKAGES_BASE_URL}/auto-explain-logs/mast
function extract_and_upload_fingerprints() {
echo "Extracting SQL query fingerprints from ${RSPEC_AUTO_EXPLAIN_LOG_PATH}"
ruby scripts/extract_fingerprints "${RSPEC_AUTO_EXPLAIN_LOG_PATH}" "${FINGERPRINTS_FILE}.new"
ruby scripts/sql_fingerprint_extractor.rb "${RSPEC_AUTO_EXPLAIN_LOG_PATH}" "${FINGERPRINTS_FILE}.new"
# Check if any new fingerprints were found
new_count=$(wc -l < "${FINGERPRINTS_FILE}.new")

View File

@ -0,0 +1,298 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'json'
require 'uri'
require 'net/http'
require 'zlib'
require 'rubygems/package'
require 'stringio'
require 'logger'
require_relative 'sql_fingerprint_extractor'
# MergeRequestQueryDiffer compares auto_explain logs from an MR against master
# to identify new query patterns introduced by the MR
class MergeRequestQueryDiffer
PROJECT_ID = ENV['CI_PROJECT_ID'] || '278964'
PACKAGE_NAME = 'auto-explain-logs'
PACKAGE_FILE = 'query-fingerprints.tar.gz'
NEW_QUERIES_PATH = 'new_sql_queries.md'
CONSOLIDATED_FINGERPRINTS_URL = ENV['CONSOLIDATED_FINGERPRINTS_URL'] ||
"https://gitlab.com/api/v4/projects/#{PROJECT_ID}/packages/generic/#{PACKAGE_NAME}/master/#{PACKAGE_FILE}"
attr_reader :mr_auto_explain_path, :output_file, :logger, :sql_fingerprint_extractor, :report_generator
def initialize(mr_auto_explain_path, logger = nil)
@mr_auto_explain_path = mr_auto_explain_path
output_dir = File.dirname(mr_auto_explain_path)
@output_file = File.join(output_dir, NEW_QUERIES_PATH)
@logger = logger || Logger.new($stdout)
@sql_fingerprint_extractor = SQLFingerprintExtractor.new(@logger)
@report_generator = ReportGenerator.new(@logger)
end
def run
logger.info "MR Query Diff: Analyzing new queries in MR compared to master"
# Step 1: Extract query fingerprints from MR
mr_queries = sql_fingerprint_extractor.extract_queries_from_file(mr_auto_explain_path)
if mr_queries.empty?
logger.info "No queries found in MR file"
write_report(output_file, "# SQL Query Analysis\n\nNo queries found in this MR.")
return 0
end
mr_fingerprints = mr_queries.filter_map { |q| q['fingerprint'] }
if mr_fingerprints.empty?
logger.info "No fingerprints found in MR queries... exiting"
return 0
end
logger.info "Found #{mr_fingerprints.size} total queries in MR"
# Step 2: Get master fingerprints
master_fingerprints = get_master_fingerprints
if master_fingerprints.empty?
logger.info "No master fingerprints found for comparison... exiting"
return 0
end
# Step 3: Compare and filter
mr_queries = filter_new_queries(mr_queries, master_fingerprints)
# Step 4: Report generation
logger.info "Final result: #{mr_queries.size} new queries compared to all master packages"
report = report_generator.generate(mr_queries)
write_report(output_file, report)
mr_queries.size
rescue StandardError => e
logger.info "Error in main execution: #{e.message}"
write_report(output_file, "# SQL Query Analysis\n\n Analysis failed: #{e.message}")
0
end
def filter_new_queries(mr_queries, master_fingerprints)
original_count = mr_queries.size
logger.info "Filtering #{original_count} queries against master fingerprints..."
# Only keep queries with fingerprints not in master set
new_queries = mr_queries.select { |q| q['fingerprint'] && Set[q['fingerprint']].disjoint?(master_fingerprints) }
filtered_count = original_count - new_queries.size
logger.info "Filtered out #{filtered_count} existing queries, #{new_queries.size} new queries found"
if new_queries.empty?
logger.info "All queries in MR are already present in master packages"
write_report(output_file, %(# SQL Query Analysis
No new SQL queries detected in this MR.
All queries in this MR are already present in master
))
end
new_queries
end
def get_master_fingerprints
logger.info "Fetching master fingerprints from consolidated package..."
fingerprints = Set.new
begin
content = download_consolidated_package
if content.nil?
logger.error "Failed to download consolidated package"
return fingerprints
end
# Extract fingerprints from the package
fingerprints = sql_fingerprint_extractor.extract_from_tar_gz(content)
logger.info "Loaded #{fingerprints.size} master fingerprints from consolidated package"
rescue StandardError => e
logger.error "Error loading master fingerprints: #{e.message}"
end
fingerprints
end
def download_consolidated_package(max_size_mb = 100)
logger.info "Downloading from: #{CONSOLIDATED_FINGERPRINTS_URL}"
url = URI(CONSOLIDATED_FINGERPRINTS_URL)
# Check file size first
begin
response = make_request(url, method: :head, parse_json: false)
if response.is_a?(Net::HTTPResponse)
content_length_mb = response['content-length'].to_i / (1024**2)
if content_length_mb > max_size_mb
logger.error "Package size (#{content_length_mb}MB) exceeds maximum allowed size (#{max_size_mb}MB)"
return
end
end
rescue StandardError => e
logger.warn "Warning: Could not validate file size: #{e}"
end
make_request(url, method: :get, parse_json: false)
end
def write_report(output_file, content)
File.write(output_file, content)
logger.info "Report saved to #{output_file}"
rescue StandardError => e
logger.error "Could not write report to file: #{e.message}"
end
def make_request(url, method: :get, parse_json: true, attempt: 1, max_attempts: 10)
if attempt >= max_attempts
logger.info "Maximum retry attempts (#{max_attempts}) reached for rate limiting"
return parse_json ? [] : nil
end
begin
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
http.read_timeout = 120
request = build_request(method, url)
if ENV['GITLAB_TOKEN']
request['PRIVATE-TOKEN'] = ENV['GITLAB_TOKEN']
elsif ENV['CI_JOB_TOKEN']
request['JOB-TOKEN'] = ENV['CI_JOB_TOKEN']
end
response = http.request(request)
case response
when Net::HTTPSuccess
return response if method == :head
if parse_json
begin
JSON.parse(response.body)
rescue JSON::ParserError => e
logger.error "Failed to parse JSON: #{e.message}"
[]
end
else
response.body
end
when Net::HTTPTooManyRequests,
Net::HTTPServerError,
Net::HTTPInternalServerError,
Net::HTTPServiceUnavailable,
Net::HTTPGatewayTimeout,
Net::HTTPBadGateway
backoff = [1 * (2**attempt), 60].min
logger.info "HTTP #{response.code} - Waiting and retrying after #{backoff} secs"
sleep(backoff)
make_request(
url, method: method, parse_json: parse_json, attempt: attempt + 1, max_attempts: max_attempts
)
else
logger.error "HTTP request failed: #{response.code} - #{response.message}"
parse_json ? [] : nil
end
rescue StandardError => e
logger.error "Error making request: #{e}"
parse_json ? [] : nil
end
end
private
def build_request(method, url)
case method
when :get
Net::HTTP::Get.new(url)
when :head
Net::HTTP::Head.new(url)
else
raise ArgumentError, "Unsupported HTTP method: #{method}"
end
end
# ReportGenerator handles creation of readable reports from query data
class ReportGenerator
attr_reader :logger
def initialize(logger)
@logger = logger || Logger.new($stdout)
end
def generate(mr_queries)
report = "# SQL Query Analysis\n\n"
if mr_queries.empty?
report += "No new SQL queries detected in this MR."
return report
end
report += "## Identified potential #{mr_queries.size} new SQL queries:\n\n"
mr_queries.each_with_index do |query, idx|
next unless query['normalized']
report += <<~DETAILS
<details>
<summary><b>Query #{idx + 1}</b>: #{format_query_summary(query)}</summary>
```sql
#{query['normalized']}
```
**Fingerprint:** `#{query['fingerprint']}`
#{query['plan'] ? format_plan(query['plan']) : ''}
</details>
DETAILS
end
report
end
def format_query_summary(query)
text = query['normalized'] || ""
cleaned = text.gsub(/\s+/, ' ').strip
cleaned.size > 80 ? "#{cleaned[0..77]}..." : cleaned
end
def format_plan(plan)
return "" unless plan
<<~PLAN
**Execution Plan:**
```json
#{
if plan.is_a?(Hash)
JSON.pretty_generate(plan)
else
plan.respond_to?(:to_s) ? plan.to_s : plan.inspect
end
}
```
PLAN
end
end
private_constant :ReportGenerator
end
if $PROGRAM_NAME == __FILE__
if ARGV.empty?
puts "Usage: #{$PROGRAM_NAME} <path/to/mr_auto_explain.ndjson[.gz]>"
exit 1
end
mr_auto_explain_path = ARGV[0]
unless File.exist?(mr_auto_explain_path)
puts "Error: File not found - #{mr_auto_explain_path}"
exit 1
end
diff = MergeRequestQueryDiffer.new(mr_auto_explain_path)
diff.run
end

View File

@ -0,0 +1,133 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'json'
require 'zlib'
require 'logger'
require 'stringio'
require 'rubygems/package'
# SQLFingerprintExtractor extracts and processes SQL query fingerprints
# from various file formats including NDJSON, gzipped NDJSON, and tar.gz archives
class SQLFingerprintExtractor
attr_reader :logger
def initialize(logger = nil)
@logger = logger || Logger.new($stdout)
end
# Extract fingerprints from a local file (compressed or uncompressed)
# Returns an array of query objects with fingerprints
def extract_queries_from_file(file_path)
logger.info "Extracting queries from file: #{file_path}" if logger
queries = []
begin
if file_path.end_with?('.gz')
Zlib::GzipReader.open(file_path) do |gz|
gz.each_line do |line|
process_json_line(line, queries)
end
end
else
File.foreach(file_path) do |line|
process_json_line(line, queries)
end
end
rescue StandardError => e
logger.warn "Warning: Error reading file: #{e.message}" if logger
return [] # Return empty array on error
end
logger.info "Extracted #{queries.size} queries from file: #{file_path}" if logger
queries
end
# Extract just the fingerprint strings from a file
# Returns a Set of fingerprint strings
def extract_fingerprints_from_file(file_path)
queries = extract_queries_from_file(file_path)
Set.new(queries.filter_map { |q| q['fingerprint'] })
end
# Extract fingerprints from a tar.gz content
# Returns a Set of fingerprint strings
def extract_from_tar_gz(content, max_size_mb = 250)
fingerprints = Set.new
max_size = max_size_mb * (1024**2) # guardrail to prevent issues if unexpectedly large
begin
io = StringIO.new(content)
gz = Zlib::GzipReader.new(io)
tar = Gem::Package::TarReader.new(gz)
tar&.each do |entry|
# Now looking for raw fingerprint files (any text file)
next unless entry.file? && !entry.directory?
# Check file size before reading
if entry.header.size > max_size
logger.error(
"File too large: #{entry.header.size / (1024**2)}MB exceeds limit #{max_size_mb}MB"
)
return fingerprints
end
entry_content = entry.read
entry_content.each_line do |line|
fingerprint = line.strip
fingerprints.add(fingerprint) unless fingerprint.empty?
end
end
rescue StandardError => e
logger.error "Error processing tar.gz: #{e.message}"
return Set.new
end
fingerprints
end
# Write a set of fingerprints to file
def write_fingerprints_to_file(fingerprints, output_file)
File.open(output_file, 'w') do |f|
fingerprints.each { |fp| f.puts(fp) }
end
logger.info "Wrote #{fingerprints.size} fingerprints to #{output_file}" if logger
end
private
def process_json_line(line, queries)
data = JSON.parse(line)
queries << data if data['fingerprint']
rescue JSON::ParserError
# Skip invalid JSON
end
end
# Command-line script functionality
if __FILE__ == $PROGRAM_NAME
if ARGV.size < 2
puts "Usage: #{$PROGRAM_NAME} <input_file> <output_file>"
exit 1
end
input_file = ARGV[0]
output_file = ARGV[1]
logger = Logger.new($stdout)
unless File.exist?(input_file)
logger.error "Input file not found - #{input_file}"
exit 1
end
begin
extractor = SQLFingerprintExtractor.new
fingerprints = extractor.extract_fingerprints_from_file(input_file)
extractor.write_fingerprints_to_file(fingerprints, output_file)
logger.info "Successfully extracted #{fingerprints.size} fingerprints to #{output_file}"
rescue StandardError => e
logger.error e.message.to_s
exit 1
end
end

View File

@ -72,13 +72,6 @@ RSpec.describe Settings, feature_category: :system_access do
end
end
describe '.attr_encrypted_db_key_base_truncated' do
it 'returns the first item from #db_key_base_keys_truncated' do
expect(described_class.attr_encrypted_db_key_base_truncated)
.to eq(described_class.db_key_base_keys_truncated.first)
end
end
describe '.db_key_base_keys_truncated' do
it 'is an array of string with maximum 32 bytes size' do
described_class.db_key_base_keys_truncated.each do |key|
@ -87,13 +80,6 @@ RSpec.describe Settings, feature_category: :system_access do
end
end
describe '.attr_encrypted_db_key_base_32' do
it 'returns the first item from #db_key_base_keys_32_bytes' do
expect(described_class.attr_encrypted_db_key_base_32)
.to eq(described_class.db_key_base_keys_32_bytes.first)
end
end
describe '.db_key_base_keys_32_bytes' do
context 'when db key base secret is less than 32 bytes' do
before do
@ -141,13 +127,6 @@ RSpec.describe Settings, feature_category: :system_access do
end
end
describe '.attr_encrypted_db_key_base' do
it 'returns the first item from #attr_encrypted_db_key_base' do
expect(described_class.attr_encrypted_db_key_base)
.to eq(described_class.db_key_base_keys.first)
end
end
describe '.db_key_base_keys' do
before do
allow(Gitlab::Application.credentials)

View File

@ -67,6 +67,14 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
next
end
# exception: `.if-default-branch-schedule-weekly` should both be set to "never"
# because the weekly job is a small subset of tests. We don't want to run either jobs.
if base['if'] == config['.if-default-branch-schedule-weekly']['if']
expect(derived).to eq(base)
expect(derived['when']).to eq('never')
next
end
# exception: `.if-merge-request-not-approved` in the base should be `.if-merge-request-approved` in derived.
# The base wants to run when the MR is approved, and the derived wants to run if it's not approved,
# and both are specifying this with `when: never`.

View File

@ -694,15 +694,22 @@ RSpec.describe MergeRequestsFinder, feature_category: :code_review_workflow do
context 'review state filtering' do
let(:params) { { review_state: 'requested_changes' } }
let(:expected_mr) { [merge_request1] }
let(:expected_mr) { [merge_request1, merge_request3] }
subject { described_class.new(user, params).execute }
before do
merge_request1.merge_request_reviewers.update_all(state: :requested_changes)
merge_request3.merge_request_reviewers.update_all(state: :requested_changes)
end
it { is_expected.to contain_exactly(*expected_mr) }
context 'when ignoring a reviewer' do
let(:params) { { review_state: 'requested_changes', ignored_reviewer_username: user2.username } }
it { is_expected.to contain_exactly(merge_request3) }
end
end
context 'multiple review state filtering' do
@ -727,6 +734,12 @@ RSpec.describe MergeRequestsFinder, feature_category: :code_review_workflow do
end
it { is_expected.to contain_exactly(*expected_mr) }
context 'when ignoring a reviewer' do
let(:params) { { not: { review_states: %w[requested_changes reviewed] }, ignored_reviewer_username: user2.username } }
it { is_expected.to contain_exactly(*expected_mr) }
end
end
end

View File

@ -139,15 +139,21 @@ describe('PipelineInputsForm', () => {
});
describe('with empty ref (error case)', () => {
beforeEach(() => {
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsErrorResponse);
});
it('handles GraphQL error', async () => {
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsErrorResponse);
await createComponent();
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching the pipeline inputs.',
message: 'ref can only be an existing branch or tag',
});
});
it('handles generic error', async () => {
pipelineInputsHandler = jest.fn().mockRejectedValue('Error');
await createComponent();
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching the pipeline inputs. Please try again.',
});
});
});

View File

@ -359,8 +359,8 @@ describe('ml/model_registry/apps/show_ml_model', () => {
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findLatestVersionLink = () => wrapper.findByTestId('sidebar-latest-version-link');
const findVersionCount = () => wrapper.findByTestId('sidebar-version-count');
const findExperimentTitle = () => wrapper.findByTestId('sidebar-experiment-title');
const findExperiment = () => wrapper.findByTestId('sidebar-experiment-label');
const findExperiment = () => wrapper.findByTestId('sidebar-experiment');
const findExperimentLabel = () => wrapper.findByTestId('sidebar-experiment-label');
it('displays sidebar author link', () => {
expect(findSidebarAuthorLink().attributes('href')).toBe('path/to/user');
@ -391,16 +391,16 @@ describe('ml/model_registry/apps/show_ml_model', () => {
});
describe('displays experiment information', () => {
it('displays experiment title', () => {
expect(findExperimentTitle().text()).toBe('Experiment');
it('displays experiment', () => {
expect(findExperiment().exists()).toBe(true);
});
it('displays experiment label', () => {
expect(findExperiment().text()).toBe('Default experiment');
expect(findExperimentLabel().text()).toBe('Default experiment');
});
it('shows a link to the default experiment', () => {
expect(findExperiment().findComponent(GlLink).attributes('href')).toBe(
expect(findExperimentLabel().findComponent(GlLink).attributes('href')).toBe(
'path/to/experiment',
);
});
@ -423,7 +423,6 @@ describe('ml/model_registry/apps/show_ml_model', () => {
});
it('does not display sidebar experiment information', () => {
expect(findExperimentTitle().exists()).toBe(false);
expect(findExperiment().exists()).toBe(false);
});
});

View File

@ -1,8 +1,9 @@
import { nextTick } from 'vue';
import { GlFormInput } from '@gitlab/ui';
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SharedProjectCreationFields from '~/projects/new_v2/components/shared_project_creation_fields.vue';
import NewProjectDestinationSelect from '~/projects/new_v2/components/project_destination_select.vue';
import { DEPLOYMENT_TARGET_SELECTIONS } from '~/projects/new_v2/form_constants';
describe('Project creation form fields component', () => {
let wrapper;
@ -23,6 +24,7 @@ describe('Project creation form fields component', () => {
},
stubs: {
GlFormInput,
GlFormSelect,
},
});
};
@ -34,6 +36,19 @@ describe('Project creation form fields component', () => {
const findProjectNameInput = () => wrapper.findByTestId('project-name-input');
const findProjectSlugInput = () => wrapper.findByTestId('project-slug-input');
const findNamespaceSelect = () => wrapper.findComponent(NewProjectDestinationSelect);
const findDeploymentTargetSelect = () => wrapper.findByTestId('deployment-target-select');
const findKubernetesHelpLink = () => wrapper.findByTestId('kubernetes-help-link');
describe('target select', () => {
it('renders the optional deployment target select', () => {
expect(findDeploymentTargetSelect().exists()).toBe(true);
expect(findKubernetesHelpLink().exists()).toBe(false);
});
it('has all the options', () => {
expect(findDeploymentTargetSelect().props('options')).toEqual(DEPLOYMENT_TARGET_SELECTIONS);
});
});
it('updates project slug according to a project name', async () => {
// NOTE: vue3 test needs the .setValue(value) and the vm.$emit('input'),

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe GitHelper do
RSpec.describe GitHelper, feature_category: :source_code_management do
describe '#short_sha' do
let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') }
@ -27,6 +27,12 @@ RSpec.describe GitHelper do
it { expect(strip_signature).to eq("this is Roger's signed tag\n\n") }
end
context 'strips SSH MESSAGE' do
let(:strip_signature) { helper.strip_signature(ssh_message_tag) }
it { expect(strip_signature).to eq("Version 1.70.0\n\n") }
end
end
def pgp_signature_tag
@ -63,6 +69,23 @@ RSpec.describe GitHelper do
SIGNATURE
end
def ssh_message_tag
<<~SIGNATURE
Version 1.70.0
-----BEGIN SSH SIGNATURE-----
iQEzBAABCAAdFiEEFMo1pwRq9j04Jovq68Q/GjfvLIoFAl2l64QACgkQ68Q/Gjfv
LIqRDggAm0d1ceVRsfldlwC6guR2ly8aWoTtZZ19E12bsfXd4lJqcQv7JXTP0og0
cwbV0l92iBJKGW6bFBipKDFmSgr5le5zFsXYOr9bJCQNOhFNMmtAgaHEIeVI16+c
S3pA+qIe516d4wRs/hcbxDJKC68iIlDaLXObdzTTLGMgbCYBFTjYJldNUfTkdvbB
oGEpFXuxV9EyfBtPLsz2vUea5GdZcRSVyJbcgm9ZU+ekdLZckroP5M0I5SQTbD3y
VrbCY3ziYtau4zK4cV66ybRz1G7tR6dcoC4kGUbaZlKsVZ1Af80agx2d9k5MR1wS
4OFe1H0zIfpPRFsyX2toaum3EX6QBA==
=hefg
-----END SSH SIGNATURE-----
SIGNATURE
end
def x509_message_tag
<<~SIGNATURE
this is Roger's signed tag

View File

@ -56,6 +56,32 @@ RSpec.describe Ci::PipelineCreation::Inputs::SpecInputs, feature_category: :pipe
expect(spec_inputs.errors).to be_empty
end
end
context 'with spec is not a hash' do
let(:specs) { 'this is a string' }
before do
stub_const('TranslationStub', Module.new do
def s_(message, *_args)
message
end
end)
described_class.include(TranslationStub)
end
it 'adds error message about invalid input specification' do
spec_inputs = described_class.new(specs)
expect(spec_inputs.errors).to include(
a_string_matching(/Invalid input specification: expected a hash-like object/)
)
end
it 'returns empty inputs' do
spec_inputs = described_class.new(specs)
expect(spec_inputs.all_inputs).to be_empty
end
end
end
describe '#all_inputs' do

View File

@ -8,7 +8,7 @@ RSpec.describe Gitlab::BackgroundMigration::EncryptCiTriggerToken, feature_categ
ci_trigger.send :attr_encrypted, :encrypted_token_tmp,
attribute: :encrypted_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
key: Settings.db_key_base_keys_32_bytes.first,
algorithm: 'aes-256-gcm',
encode: false
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::TruncateTaggings, feature_category: :database do
include MigrationsHelpers
before do
stub_feature_flags(disallow_database_ddl_feature_flags: false)
end
let(:taggings) { table(:taggings, database: :ci) }
describe '#execute' do
context 'when the table has data' do
before do
taggings.create!
end
context 'when executed on .com' do
before do
allow(Gitlab).to receive(:com_except_jh?).and_return(true)
end
it 'truncates taggings' do
recorder = ActiveRecord::QueryRecorder.new { described_class.new.execute }
expect(recorder.log).to include(/TRUNCATE TABLE "taggings"/)
end
end
it 'is a no-op everywhere else' do
recorder = ActiveRecord::QueryRecorder.new { described_class.new.execute }
expect(recorder.log).to be_empty
end
end
context 'when the table is empty' do
context 'when executed on .com' do
before do
allow(Gitlab).to receive(:com_except_jh?).and_return(true)
end
it 'does not truncate taggings' do
recorder = ActiveRecord::QueryRecorder.new { described_class.new.execute }
expect(recorder.log).not_to include(/TRUNCATE TABLE "taggings"/)
end
end
end
end
end

View File

@ -252,8 +252,9 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
describe '.review_states' do
let(:states) { MergeRequestReviewer.states[:requested_changes] }
let(:ignored_reviewer) { nil }
subject(:merge_requests) { described_class.review_states(states) }
subject(:merge_requests) { described_class.review_states(states, ignored_reviewer) }
it 'returns MRs that have a reviewer with the passed state' do
expect(merge_requests).to eq([merge_request1])
@ -264,14 +265,32 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { expect(merge_requests).to match_array([merge_request1, merge_request2]) }
end
context 'when ignoring a reviewer' do
let(:states) { [MergeRequestReviewer.states[:reviewed], MergeRequestReviewer.states[:requested_changes]] }
let(:ignored_reviewer) { user1 }
it { expect(merge_requests).to contain_exactly(merge_request2) }
end
end
describe '.no_review_states' do
let(:states) { [MergeRequestReviewer.states[:requested_changes]] }
let(:ignored_reviewer) { nil }
subject(:merge_requests) { described_class.no_review_states(states) }
subject(:merge_requests) { described_class.no_review_states(states, ignored_reviewer) }
it { expect(merge_requests).to contain_exactly(merge_request2) }
context 'when ignoring a reviewer' do
let(:ignored_reviewer) { user2 }
before_all do
merge_request2.merge_request_reviewers.find_by(user_id: user2.id).update!(state: :requested_changes)
end
it { expect(merge_requests).to contain_exactly(merge_request2) }
end
end
describe '.assignee_or_reviewer' do

View File

@ -222,7 +222,7 @@ RSpec.describe 'Query.project.ciPipelineCreationInputs', feature_category: :pipe
post_graphql(query, current_user: user)
expect(graphql_errors)
.to include(a_hash_including('message' => 'ref can only be an existing branch or tag'))
.to include(a_hash_including('message' => 'Can only run new pipelines for an existing branch or tag'))
end
end
end

View File

@ -0,0 +1,398 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../scripts/merge_request_query_differ'
RSpec.describe MergeRequestQueryDiffer, feature_category: :tooling do
let(:logger) { instance_double(Logger) }
let(:sql_fingerprint_extractor) { instance_double(SQLFingerprintExtractor) }
let(:file_content) do
%(
{"fingerprint":"def456","normalized":"SELECT * FROM projects WHERE user_id = $1"}
{"normalized":"SELECT * FROM issues"}
invalid json line,
{"fingerprint":"abc123","normalized":"SELECT * FROM users WHERE id = $1"}
)
end
let(:empty_file) { Tempfile.new(%w[mr_auto_explain.ndjson]) }
let(:temp_file) { Tempfile.new(%w[mr_auto_explain.ndjson]) }
before do
allow(Logger).to receive(:new).and_return(logger)
allow(SQLFingerprintExtractor).to receive(:new).and_return(sql_fingerprint_extractor)
allow(logger).to receive_messages(info: nil, warn: nil, error: nil)
allow(differ).to receive(:write_report)
end
subject(:differ) { described_class.new(empty_file.path, logger) }
describe "#run" do
context "when no queries are found in MR" do
it "exits early and writes an appropriate report" do
allow(sql_fingerprint_extractor).to receive(:extract_queries_from_file).and_return([])
result = differ.run
expect(result).to eq(0)
expect(differ).to have_received(:write_report).with(
differ.output_file,
"# SQL Query Analysis\n\nNo queries found in this MR."
)
end
end
context "when queries without fingerprints are found in MR" do
it "exits early without further processing" do
allow(differ).to receive(:get_master_fingerprints)
allow(sql_fingerprint_extractor).to receive(:extract_queries_from_file)
.and_return([{ 'normalized' => 'SELECT * FROM issues' }])
result = differ.run
expect(differ).not_to have_received(:get_master_fingerprints)
expect(result).to eq(0)
end
end
context "when no master fingerprints are found" do
it "exits early without comparing queries" do
allow(sql_fingerprint_extractor).to receive(:extract_queries_from_file)
.and_return([{ 'fingerprint' => 'fp1', 'normalized' => 'SELECT * FROM users' }])
allow(differ).to receive(:get_master_fingerprints).and_return(Set.new)
allow(differ).to receive(:filter_new_queries)
result = differ.run
expect(differ).not_to have_received(:filter_new_queries)
expect(result).to eq(0)
end
end
context "when everything works as expected" do
it "processes the entire pipeline from extraction to report generation" do
allow(sql_fingerprint_extractor).to receive(:extract_queries_from_file)
.and_return([
{ 'fingerprint' => 'fp3', 'normalized' => 'SELECT * FROM issues' },
{ 'fingerprint' => 'fp1', 'normalized' => 'SELECT * FROM users' },
{ 'fingerprint' => 'fp2', 'normalized' => 'SELECT * FROM projects' }
])
allow(differ).to receive(:get_master_fingerprints).and_return(Set.new(['fp1']))
allow(differ.report_generator).to receive(:generate).and_return("# Sample Test Report")
result = differ.run
expect(differ).to have_received(:write_report).with(differ.output_file, "# Sample Test Report")
expect(result).to eq(2) # Two new queries (fp2 and fp3)
end
end
context "when errors occur" do
it "handles errors gracefully" do
allow(sql_fingerprint_extractor).to receive(:extract_queries_from_file)
.and_raise(StandardError.new("Test error"))
result = differ.run
expect(differ).to have_received(:write_report).with(
differ.output_file,
"# SQL Query Analysis\n\n Analysis failed: Test error"
)
expect(result).to eq(0)
end
end
end
describe "#get_master_fingerprints" do
it "downloads and extracts fingerprints from the consolidated package" do
package_content = "mock_package_content"
master_fingerprints = Set.new(%w[fd96528f933e7661 b10ab3c1b7bf923e b65a3b193bb3d1fb])
allow(differ).to receive(:download_consolidated_package).and_return(package_content)
allow(sql_fingerprint_extractor).to receive(:extract_from_tar_gz)
.with(package_content)
.and_return(master_fingerprints)
result = differ.get_master_fingerprints
expect(result).to be_a(Set)
expect(result.size).to eq(3)
expect(result).to eq(master_fingerprints)
end
it "handles download failures" do
allow(differ).to receive(:download_consolidated_package).and_return(nil)
result = differ.get_master_fingerprints
expect(result).to be_a(Set)
expect(result).to be_empty
expect(logger).to have_received(:error).with("Failed to download consolidated package")
end
it "handles extraction errors" do
allow(differ).to receive(:download_consolidated_package).and_return("package_content")
allow(sql_fingerprint_extractor).to receive(:extract_from_tar_gz)
.and_raise(StandardError.new("Extraction error"))
result = differ.get_master_fingerprints
expect(result).to be_a(Set)
expect(result).to be_empty
expect(logger).to have_received(:error).with("Error loading master fingerprints: Extraction error")
end
end
describe "#filter_new_queries" do
let(:mr_queries) do
[
{ 'fingerprint' => 'fp3', 'normalized' => 'SELECT * FROM issues' },
{ 'fingerprint' => 'fp1', 'normalized' => 'SELECT * FROM users' },
{ 'fingerprint' => 'fp2', 'normalized' => 'SELECT * FROM projects' }
]
end
it "identifies queries with fingerprints not present in master" do
master_fingerprints = Set.new(['fp2'])
result = differ.filter_new_queries(mr_queries, master_fingerprints)
expect(result.pluck('fingerprint')).to contain_exactly('fp1', 'fp3')
end
it "filters out all queries when all fingerprints are in master" do
master_fingerprints = Set.new(%w[fp2 fp1 fp3])
result = differ.filter_new_queries(mr_queries, master_fingerprints)
expect(result).to be_empty
end
it "writes a report when no new queries are found" do
master_fingerprints = Set.new(%w[fp2 fp1 fp3])
differ.filter_new_queries(mr_queries, master_fingerprints)
expect(differ).to have_received(:write_report).with(differ.output_file, /No new SQL queries detected in this MR/)
end
end
describe "#download_consolidated_package" do
let(:url) { URI(MergeRequestQueryDiffer::CONSOLIDATED_FINGERPRINTS_URL) }
let(:max_size_mb) { 10 }
it "downloads the package when file size is acceptable" do
head_response = instance_double(Net::HTTPSuccess, is_a?: true, :[] => "5242880") # 5MB
package_content = "mock package content"
allow(differ).to receive(:make_request).with(url, method: :head, parse_json: false).and_return(head_response)
allow(differ).to receive(:make_request).with(url, method: :get, parse_json: false).and_return(package_content)
result = differ.download_consolidated_package(max_size_mb)
expect(result).to eq(package_content)
end
it "aborts download when file size is too large" do
head_response = instance_double(Net::HTTPSuccess, is_a?: true, :[] => ((max_size_mb + 1) * (1024**2)).to_s) # 11MB
allow(differ).to receive(:make_request).with(url, method: :head, parse_json: false).and_return(head_response)
allow(differ).to receive(:make_request).with(url, method: :get, parse_json: false)
result = differ.download_consolidated_package(max_size_mb)
expect(differ).to have_received(:make_request).with(url, method: :head, parse_json: false)
expect(differ).not_to have_received(:make_request).with(url, method: :get, parse_json: false)
expect(result).to be_nil
end
it "proceeds with download when size check fails" do
package_content = "mock package content"
allow(differ).to receive(:make_request)
.with(url, method: :head, parse_json: false)
.and_raise(StandardError.new("Size check failed"))
allow(differ).to receive(:make_request)
.with(url, method: :get, parse_json: false)
.and_return(package_content)
result = differ.download_consolidated_package(max_size_mb)
expect(result).to eq(package_content)
expect(logger).to have_received(:warn).with(/Warning: Could not validate file size/)
expect(differ).to have_received(:make_request).with(url, method: :get, parse_json: false)
end
end
describe "#make_request" do
let(:test_url) { URI("https://gitlab.example.com/foo/bar") }
let(:http) { instance_double(Net::HTTP) }
let(:request) { instance_double(Net::HTTP::Get) }
let(:success_response) { Net::HTTPSuccess.new('1.1', '200', 'OK') }
before do
allow(Net::HTTP).to receive(:new).with(any_args).and_return(http)
allow(http).to receive(:use_ssl=)
allow(http).to receive(:read_timeout=)
allow(success_response).to receive(:body).and_return('{"data":"success"}')
allow(http).to receive(:request).and_return(success_response)
end
context "with authentication headers" do
before do
allow(Net::HTTP::Get).to receive(:new).and_return(request)
allow(request).to receive(:[]=)
end
it "set PRIVATE-TOKEN when GITLAB_TOKEN present" do
stub_env('GITLAB_TOKEN', "test-gitlab-token")
differ.make_request(test_url)
expect(request).to have_received(:[]=).with('PRIVATE-TOKEN', "test-gitlab-token")
end
it "set JOB-TOKEN when CI_JOB_TOKEN present" do
stub_env('CI_JOB_TOKEN', "test-ci-job-token")
differ.make_request(test_url)
expect(request).to have_received(:[]=).with('JOB-TOKEN', "test-ci-job-token")
end
it "prefers GITLAB_TOKEN over CI_JOB_TOKEN" do
stub_env('CI_JOB_TOKEN', "test-ci-job-token")
stub_env('GITLAB_TOKEN', "test-gitlab-token")
differ.make_request(test_url)
expect(request).to have_received(:[]=).with('PRIVATE-TOKEN', "test-gitlab-token")
expect(request).not_to have_received(:[]=).with('JOB-TOKEN', "test-ci-job-token")
end
end
it "stops retrying after max attempts" do
result = differ.make_request(test_url, attempt: 4, max_attempts: 3)
expect(result).to eq([])
expect(logger).to have_received(:info).with("Maximum retry attempts (3) reached for rate limiting")
end
it "returns parsed JSON for successful requests" do
result = differ.make_request(test_url)
expect(result).to eq({ "data" => "success" })
end
it "returns raw response body when parse_json is false" do
allow(success_response).to receive(:body).and_return('raw response data')
result = differ.make_request(test_url, parse_json: false)
expect(result).to eq('raw response data')
end
it "supports HEAD requests" do
result = differ.make_request(test_url, method: :head, parse_json: false)
expect(result).to eq(success_response)
end
context "when handling errors" do
it "retries on common server errors" do
allow(http).to receive(:request).and_return(
Net::HTTPServiceUnavailable.new('1.1', '503', 'Service Unavailable'),
Net::HTTPTooManyRequests.new('1.1', '429', 'Too Many Requests'),
success_response
)
allow(differ).to receive(:sleep)
result = differ.make_request(test_url)
expect(result).to eq({ "data" => "success" })
end
it "returns empty json when parse_json is true" do
allow(http).to receive(:request).and_return(Net::HTTPFatalError)
expect(differ.make_request(test_url, method: :get, parse_json: true)).to eq([])
end
it "returns nil when parse json is false" do
allow(http).to receive(:request).and_return(Net::HTTPFatalError)
expect(differ.make_request(test_url, method: :get, parse_json: false)).to be_nil
end
it "logs error when resource not found" do
allow(http).to receive(:request).and_return(Net::HTTPNotFound.new('1.1', '404', 'Test 404'))
expect(differ.make_request(test_url, method: :get, parse_json: true)).to eq([])
expect(logger).to have_received(:error).with(/HTTP request failed: 404 - Test 404/)
end
it "logs error if an unsupported method is passed" do
result = differ.make_request(test_url, method: :put, parse_json: false)
expect(result).to be_nil
expect(logger).to have_received(:error).with(/Error making request: Unsupported HTTP method: put/)
end
it "returns nil and logs error when exception occurs" do
allow(http).to receive(:request).and_raise(StandardError.new("Testing Error"))
result = differ.make_request(test_url, parse_json: false)
expect(result).to be_nil
expect(logger).to have_received(:error).with("Error making request: Testing Error")
end
it "returns empty array and logs error when JSON parsing fails" do
allow(success_response).to receive(:body).and_return('invalid json')
result = differ.make_request(test_url)
expect(result).to eq([])
expect(logger).to have_received(:error).with(/Failed to parse JSON/)
end
end
end
describe "#write_report" do
subject(:differ) { described_class.new(empty_file, logger) }
before do
allow(logger).to receive(:info)
allow(differ).to receive(:write_report).and_call_original
allow(File).to receive(:write)
end
it "writes content to file and logs success" do
differ.write_report("test.txt", "content")
expect(logger).to have_received(:info).with("Report saved to test.txt")
end
it "logs errors when file write fails" do
allow(File).to receive(:write).and_raise(StandardError.new("Write error"))
differ.write_report("test.txt", "content")
expect(logger).to have_received(:error).with("Could not write report to file: Write error")
end
end
describe "ReportGenerator" do
let(:report_generator) { differ.report_generator }
it "generates a report with new queries" do
report = report_generator.generate([{ 'fingerprint' => 'fp1', 'normalized' => 'SELECT * FROM users' }])
expect(report).to include("# SQL Query Analysis")
expect(report).to include("Identified potential 1 new SQL queries")
expect(report).to include("Query 1")
expect(report).to include("SELECT * FROM users")
expect(report).to include("fp1")
end
it "includes execution plans when available" do
report = report_generator.generate([
{ 'fingerprint' => 'fp1', 'normalized' => 'SELECT * FROM users', 'plan' => { 'Node Type' => 'Index Scan' } }
])
expect(report).to include("Execution Plan")
expect(report).to include("Index Scan")
end
it "handles empty query list" do
report = report_generator.generate([])
expect(report).to include("No new SQL queries detected in this MR")
end
it "formats hash plan" do
hash_plan = { 'Node Type' => 'Index Scan' }
result = report_generator.format_plan(hash_plan)
expect(result).to include(' "Node Type": "Index Scan"')
end
it "formats non hash plan" do
result = report_generator.format_plan(1234)
expect(result).to include('1234')
end
end
end

View File

@ -0,0 +1,194 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../scripts/sql_fingerprint_extractor'
RSpec.describe SQLFingerprintExtractor, feature_category: :tooling do
let(:logger) { instance_double(Logger, info: nil, warn: nil, error: nil) }
let(:extractor) { described_class.new(logger) }
describe '#initialize' do
it 'uses the provided logger' do
expect(extractor.logger).to eq(logger)
end
it 'creates a default logger if none provided' do
allow(Logger).to receive(:new).with($stdout).and_call_original
expect(described_class.new.logger).to be_a(Logger)
end
end
describe '#extract_queries_from_file' do
context 'with a regular text file' do
let(:file_path) { 'test_queries.ndjson' }
let(:valid_line) { '{"fingerprint":"def123","normalized":"SELECT * FROM accounts"}' }
let(:second_valid_line) { '{"fingerprint":"abc123","normalized":"SELECT * FROM users"}' }
let(:invalid_line) { 'invalid json' }
let(:no_fingerprint) { '{"normalized":"SELECT * FROM users"}' }
before do
allow(File).to receive(:foreach)
.with(file_path)
.and_yield(valid_line)
.and_yield(invalid_line)
.and_yield(no_fingerprint)
.and_yield(second_valid_line)
end
it 'extracts valid queries with fingerprints' do
queries = extractor.extract_queries_from_file(file_path)
expect(queries.size).to eq(2)
expect(queries.first['fingerprint']).to eq('def123')
expect(queries.first['normalized']).to eq('SELECT * FROM accounts')
expect(queries.second['fingerprint']).to eq('abc123')
expect(queries.second['normalized']).to eq('SELECT * FROM users')
end
it 'logs the extraction process' do
expect(logger).to receive(:info).with("Extracting queries from file: #{file_path}")
expect(logger).to receive(:info).with(/Extracted \d+ queries from file:/)
extractor.extract_queries_from_file(file_path)
end
end
context 'with a gzipped file' do
let(:gz_file_path) { 'test_queries.ndjson.gz' }
let(:gz_reader) { instance_double(Zlib::GzipReader) }
let(:valid_line) { '{"fingerprint":"def456","normalized":"SELECT * FROM posts"}' }
before do
allow(Zlib::GzipReader).to receive(:open).with(gz_file_path).and_yield(gz_reader)
allow(gz_reader).to receive(:each_line).and_yield(valid_line)
end
it 'extracts queries from a gzipped file' do
queries = extractor.extract_queries_from_file(gz_file_path)
expect(queries.size).to eq(1)
expect(queries.first['fingerprint']).to eq('def456')
end
end
context 'when an error occurs' do
let(:file_path) { 'nonexistent_file.ndjson' }
before do
allow(File).to receive(:foreach).with(file_path).and_raise(StandardError.new('File read error'))
end
it 'logs the error and returns an empty array' do
expect(logger).to receive(:warn).with("Warning: Error reading file: File read error")
queries = extractor.extract_queries_from_file(file_path)
expect(queries).to be_empty
end
end
end
describe '#extract_fingerprints_from_file' do
let(:file_path) { 'test_queries.ndjson' }
let(:queries) { [{ 'fingerprint' => 'abc123' }, { 'fingerprint' => 'def456' }, {}] }
before do
allow(extractor).to receive(:extract_queries_from_file).with(file_path).and_return(queries)
end
it 'extracts only the fingerprints as a Set' do
fingerprints = extractor.extract_fingerprints_from_file(file_path)
expect(fingerprints).to be_a(Set)
expect(fingerprints.size).to eq(2)
expect(fingerprints).to include('abc123', 'def456')
end
end
describe '#extract_from_tar_gz' do
let(:tar_gz_content) { 'mock_tar_gz_content' }
let(:string_io) { instance_double(StringIO) }
let(:gzip_reader) { instance_double(Zlib::GzipReader) }
let(:tar_reader) { instance_double(Gem::Package::TarReader) }
let(:entry) { instance_double(Gem::Package::TarReader::Entry, file?: true, directory?: false) }
let(:entry_header) { instance_double(Gem::Package::TarHeader, size: 1000) }
let(:entry_content) { "fingerprint1\nfingerprint2\n" }
before do
allow(StringIO).to receive(:new).with(tar_gz_content).and_return(string_io)
allow(Zlib::GzipReader).to receive(:new).with(string_io).and_return(gzip_reader)
allow(Gem::Package::TarReader).to receive(:new).with(gzip_reader).and_return(tar_reader)
allow(tar_reader).to receive(:each).and_yield(entry)
allow(entry).to receive_messages(header: entry_header, read: entry_content)
end
it 'extracts fingerprints from tar.gz content' do
fingerprints = extractor.extract_from_tar_gz(tar_gz_content)
expect(fingerprints).to be_a(Set)
expect(fingerprints.size).to eq(2)
expect(fingerprints).to include('fingerprint1', 'fingerprint2')
end
context 'when file is too large' do
let(:max_size_mb) { 0.001 } # 1KB max
let(:entry_header) { instance_double(Gem::Package::TarHeader, size: 2000) } # 2KB file size
it 'logs the error and returns empty set' do
expect(logger).to receive(:error).with(/File too large:/)
fingerprints = extractor.extract_from_tar_gz(tar_gz_content, max_size_mb)
expect(fingerprints).to be_a(Set)
expect(fingerprints).to be_empty
end
end
context 'when an error occurs' do
before do
allow(StringIO).to receive(:new).with(tar_gz_content).and_raise(StandardError.new('Tar.gz processing error'))
end
it 'logs the error and returns empty set' do
expect(logger).to receive(:error).with("Error processing tar.gz: Tar.gz processing error")
fingerprints = extractor.extract_from_tar_gz(tar_gz_content)
expect(fingerprints).to be_a(Set)
expect(fingerprints).to be_empty
end
end
end
describe '#write_fingerprints_to_file' do
let(:fingerprints) { Set.new(%w[abc123 def456]) }
let(:output_file) { 'output_fingerprints.txt' }
let(:file) { instance_double(File) }
before do
allow(File).to receive(:open).with(output_file, 'w').and_yield(file)
allow(file).to receive(:puts)
end
it 'writes each fingerprint to the file' do
expect(file).to receive(:puts).with('abc123')
expect(file).to receive(:puts).with('def456')
extractor.write_fingerprints_to_file(fingerprints, output_file)
end
it 'logs the number of fingerprints written' do
expect(logger).to receive(:info).with("Wrote 2 fingerprints to output_fingerprints.txt")
extractor.write_fingerprints_to_file(fingerprints, output_file)
end
end
describe '#process_json_line' do
let(:queries) { [] }
it 'adds valid queries with fingerprints' do
extractor.send(:process_json_line, '{"fingerprint":"abc123"}', queries)
expect(queries.size).to eq(1)
expect(queries.first['fingerprint']).to eq('abc123')
end
it 'skips lines without fingerprints' do
extractor.send(:process_json_line, '{"other":"value"}', queries)
expect(queries).to be_empty
end
it 'handles JSON parse errors' do
extractor.send(:process_json_line, 'invalid json', queries)
expect(queries).to be_empty
end
end
end

View File

@ -74,7 +74,7 @@ RSpec.describe Ci::PipelineCreation::FindPipelineInputsService, feature_category
result = service.execute
expect(result).to be_error
expect(result.message).to eq('insufficient permissions to read inputs')
expect(result.message).to eq(s_('Pipelines|Insufficient permissions to read inputs'))
end
end
@ -90,7 +90,7 @@ RSpec.describe Ci::PipelineCreation::FindPipelineInputsService, feature_category
result = service.execute
expect(result).to be_error
expect(result.message).to eq('ref can only be an existing branch or tag')
expect(result.message).to eq(s_('Pipelines|Can only run new pipelines for an existing branch or tag'))
end
end
@ -101,7 +101,7 @@ RSpec.describe Ci::PipelineCreation::FindPipelineInputsService, feature_category
result = service.execute
expect(result).to be_error
expect(result.message).to eq('ref can only be an existing branch or tag')
expect(result.message).to eq(s_('Pipelines|Can only run new pipelines for an existing branch or tag'))
end
end
@ -143,7 +143,7 @@ RSpec.describe Ci::PipelineCreation::FindPipelineInputsService, feature_category
result = service.execute
expect(result).to be_error
expect(result.message).to eq('invalid YAML config')
expect(result.message).to eq(s_('Pipelines|Invalid YAML syntax'))
end
end
end
@ -191,7 +191,7 @@ RSpec.describe Ci::PipelineCreation::FindPipelineInputsService, feature_category
result = service.execute
expect(result).to be_error
expect(result.message).to eq('inputs not supported for this CI config source')
expect(result.message).to eq(s_('Pipelines|Inputs not supported for this CI config source'))
end
end
end

View File

@ -78,7 +78,8 @@ RSpec.describe Ci::PipelineTriggers::CreateService, feature_category: :continuou
max_expiry_date = Date.current.advance(days: PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS)
error_text = format(_("must be before %{expiry_date}"), expiry_date: max_expiry_date)
it 'fails validation when trigger_token_expiration feature flag on' do
it 'fails validation when trigger_token_expiration feature flag on',
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/524233' do
stub_feature_flags(trigger_token_expiration: true)
response = service.execute

View File

@ -23,6 +23,7 @@ import { FixedRubyPlugin } from './config/helpers/vite_plugin_ruby_fixed.mjs';
import { StylePlugin } from './config/helpers/vite_plugin_style.mjs';
import { IconsPlugin } from './config/helpers/vite_plugin_icons.mjs';
import { ImagesPlugin } from './config/helpers/vite_plugin_images.mjs';
import { CrossOriginWorkerPlugin } from './config/helpers/vite_plugin_cross_origin_worker';
let viteGDKConfig;
try {
@ -110,6 +111,7 @@ export default defineConfig({
viteCommonjs({
include: [path.resolve(javascriptsPath, 'locale/ensure_single_line.cjs')],
}),
CrossOriginWorkerPlugin(),
],
define: {
// window can be undefined in a Web Worker
@ -130,6 +132,8 @@ export default defineConfig({
'process.env.PDF_JS_CMAPS_UBLIC_PATH': JSON.stringify(PDF_JS_CMAPS_PUBLIC_PATH),
},
server: {
// this fixes Vite server being unreachable on some configurations
host: '0.0.0.0',
cors: true,
warmup: {
clientFiles: ['javascripts/entrypoints/main.js', 'javascripts/entrypoints/super_sidebar.js'],