diff options
103 files changed, 1935 insertions, 1034 deletions
diff --git a/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml b/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml index c6fa9a9e179..cad12b8b5cb 100644 --- a/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml +++ b/.gitlab/ci/gitlab-com/danger-review.gitlab-ci.yml @@ -1,6 +1,6 @@ include: - project: gitlab-org/quality/pipeline-common - ref: 7.8.0 + ref: 7.10.2 file: - /ci/danger-review.yml diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 30dff07e3e1..4e27baf2fbe 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -754,13 +754,38 @@ rspec system pg14-as-if-foss clusterwide-db: rspec-ee unit gitlab-duo-chat pg14: variables: REAL_AI_REQUEST: "true" - VERTEX_AI_EMBEDDINGS: "true" + RSPEC_RETRY_RETRY_COUNT: 0 extends: - .rspec-ee-base-pg14 - - .rails:rules:ee-gitlab-duo-chat-vertex-ai + - .rails:rules:ee-gitlab-duo-chat-base + parallel: + matrix: + - DUO_RSPEC: ["lib/gitlab/llm/chain/agents/zero_shot/executor_real_requests_spec.rb", "support_specs/helpers/chat_qa_evaluation_helpers_spec.rb"] script: - !reference [.base-script, script] - - rspec_paralellized_job "--fail-fast=${RSPEC_FAIL_FAST_THRESHOLD} --tag real_ai_request" + - bundle exec rspec -Ispec -rspec_helper --failure-exit-code 0 --tag real_ai_request --color -- ee/spec/${DUO_RSPEC} + +rspec-ee unit gitlab-duo-chat-qa pg14: + variables: + REAL_AI_REQUEST: "true" + RSPEC_RETRY_RETRY_COUNT: 0 + extends: + - .rspec-ee-base-pg14 + - .rails:rules:ee-gitlab-duo-chat-base + parallel: + matrix: + - DUO_RSPEC: ["qa_epic_spec.rb", "qa_issue_spec.rb"] + script: + - !reference [.base-script, script] + - source ./scripts/utils.sh + - install_gitlab_gem + - bundle exec rspec -Ispec -rspec_helper --failure-exit-code 0 --tag real_ai_request --color -- ee/spec/lib/gitlab/llm/chain/agents/zero_shot/${DUO_RSPEC} + - ./scripts/duo_chat/reporter.rb + artifacts: + expire_in: 5d + paths: + - tmp/duo_chat/qa*.json + - "${DUO_RSPEC}.md" rspec-ee migration pg14: extends: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 336e2df80f7..4c95c3b6906 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -65,7 +65,7 @@ .if-merge-request-labels-run-in-ruby3_1: &if-merge-request-labels-run-in-ruby3_1 if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_1/' -.if-merge-request-labels-run-in-non-default-ruby: &if-merge-request-labels-run-in-non-default-ruby +.if-merge-request-labels-run-in-ruby3_2: &if-merge-request-labels-run-in-ruby3_2 if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_2/' .if-merge-request-labels-as-if-foss: &if-merge-request-labels-as-if-foss @@ -2117,20 +2117,14 @@ when: never - if: '$ANTHROPIC_API_KEY == null' when: never - - <<: *if-merge-request - changes: *ai-patterns - when: manual - allow_failure: true - -.rails:rules:ee-gitlab-duo-chat-vertex-ai: - rules: - - !reference [".rails:rules:ee-gitlab-duo-chat-base", rules] - if: '$VERTEX_AI_PROJECT == null' when: never - if: '$VERTEX_AI_CREDENTIALS == null' when: never - - if: '$VERTEX_AI_EMBEDDINGS == null' - when: never + - <<: *if-merge-request + changes: *ai-patterns + when: manual + allow_failure: true .rails:rules:as-if-foss-migration: rules: @@ -2746,7 +2740,7 @@ .setup:rules:verify-default-ruby: rules: - - <<: *if-merge-request-labels-run-in-non-default-ruby + - <<: *if-merge-request-labels-run-in-ruby3_2 .setup:rules:verify-tests-yml: rules: diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 12afa173640..9f9ff6e5808 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -5867f71c734a055e3af41e237ce19f992dd68120 +9bf28d2089501b82b40e2b9f6ad21cf80751f15f @@ -537,7 +537,7 @@ gem 'kas-grpc', '~> 0.2.0' # rubocop:todo Gemfile/MissingFeatureCategory gem 'grpc', '~> 1.58.0' # rubocop:todo Gemfile/MissingFeatureCategory -gem 'google-protobuf', '~> 3.24', '>= 3.24.4' # rubocop:todo Gemfile/MissingFeatureCategory +gem 'google-protobuf', '~> 3.25' # rubocop:todo Gemfile/MissingFeatureCategory gem 'toml-rb', '~> 2.2.0' # rubocop:todo Gemfile/MissingFeatureCategory diff --git a/Gemfile.checksum b/Gemfile.checksum index 018f49074cf..b7ad54aef17 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -243,16 +243,16 @@ {"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"}, {"name":"google-cloud-profiler-v2","version":"0.4.0","platform":"ruby","checksum":"53fc2ab175d08f54233c644310d47798feac996220916815c4fb44c937b5d3e3"}, {"name":"google-cloud-storage","version":"1.44.0","platform":"ruby","checksum":"299a1e055c9277c8120f7c10d21d37e4d8c17c7b963350c0e0bff7e9d9a570ea"}, -{"name":"google-protobuf","version":"3.24.4","platform":"aarch64-linux","checksum":"d3e824753a9511e4c08439586069a636c23d9ca16a509f316a895353c11a1ac8"}, -{"name":"google-protobuf","version":"3.24.4","platform":"arm64-darwin","checksum":"e13b12a648668d99d8b71ffcf378bfd744885af11e983460677073b2c8e2a979"}, -{"name":"google-protobuf","version":"3.24.4","platform":"java","checksum":"657d67b5425afa0beb94e54df7d0a15da3daa45a500fc252e7550806669b47f1"}, -{"name":"google-protobuf","version":"3.24.4","platform":"ruby","checksum":"38a403ca2fd905d3ed7c20f8d2e4718af1be3eb99093d35d7021383f6e72f2ca"}, -{"name":"google-protobuf","version":"3.24.4","platform":"x64-mingw-ucrt","checksum":"fc0396dd9f45ea54d494097e0077ee8c0cc002f1c825f06ed40f4e3b4de6948c"}, -{"name":"google-protobuf","version":"3.24.4","platform":"x64-mingw32","checksum":"ba1b5cd5effa6c6a738eb2d2d0701e3d83d95b81842564b5feb9c42579722fc6"}, -{"name":"google-protobuf","version":"3.24.4","platform":"x86-linux","checksum":"f9cae6c878381da082eab1d3eceb84525c7d7413401e1a4a5ee179b66fdbebe0"}, -{"name":"google-protobuf","version":"3.24.4","platform":"x86-mingw32","checksum":"4cf31ca7d447a86200dfcb86f64ddd046c1f9c96dc537c9d74ab19e0c36a8f0b"}, -{"name":"google-protobuf","version":"3.24.4","platform":"x86_64-darwin","checksum":"44d541e980989f1aa007a3c5481ec93932b2e50cfa63c22427dd42460a5d2865"}, -{"name":"google-protobuf","version":"3.24.4","platform":"x86_64-linux","checksum":"68f65302fad9f47c88d38136fda0dec6078f3a2a79fb5bcccf62121fd8fcca50"}, +{"name":"google-protobuf","version":"3.25.0","platform":"aarch64-linux","checksum":"4455602758a60bd698a57c7210efc440523523fbe0c0c712624e57bb02c6c9d4"}, +{"name":"google-protobuf","version":"3.25.0","platform":"arm64-darwin","checksum":"c1ba0bb5504155f5bd0d11d649316ff52cef5b2a1e7ce876497815f98be3c5a6"}, +{"name":"google-protobuf","version":"3.25.0","platform":"java","checksum":"7006d8485d6c729c081a7eb8592d8c494fd8716863a7fb7ad7c76188eafc41a5"}, +{"name":"google-protobuf","version":"3.25.0","platform":"ruby","checksum":"b51632d900b633fbd6164784351bee93001dfd3f32bd18f6505fc97d64e1a1a1"}, +{"name":"google-protobuf","version":"3.25.0","platform":"x64-mingw-ucrt","checksum":"e4935e41e0f3c32fe96e496803de61d36273474ebb72ac7ee9db3a4ecb4b5cd6"}, +{"name":"google-protobuf","version":"3.25.0","platform":"x64-mingw32","checksum":"42b13346a1be8346e4d62a41ac7150f374ca5d254b3bb4bf3bc817de522ce969"}, +{"name":"google-protobuf","version":"3.25.0","platform":"x86-linux","checksum":"7f391788f013778ffae197a184481ff24265a977d1cb2270b13a5af1ba2f53d5"}, +{"name":"google-protobuf","version":"3.25.0","platform":"x86-mingw32","checksum":"2f42a5738af0a874b35b228a2df8de21a58fa265627cfd0fa57edaea160c1087"}, +{"name":"google-protobuf","version":"3.25.0","platform":"x86_64-darwin","checksum":"c3d4a144d8f4d61193ab1a4c5d52e2d40562ba13e07eeca1fca34bc59212c352"}, +{"name":"google-protobuf","version":"3.25.0","platform":"x86_64-linux","checksum":"c6a76175c921b300ee62b21d36e8a9c07f0a4967a17be0671a83c57d7bf9bd0f"}, {"name":"googleapis-common-protos","version":"1.4.0","platform":"ruby","checksum":"da2380fb5ab1563580816c74e8d684ac17512c3654c829a3ee84f6d6139de382"}, {"name":"googleapis-common-protos-types","version":"1.5.0","platform":"ruby","checksum":"5769cf7376abc86ef7f5897a4aaca1d5c5a3c49ddabeddd2c251fcf8155f858b"}, {"name":"googleauth","version":"1.3.0","platform":"ruby","checksum":"51dd7362353cf1e90a2d01e1fb94321ae3926c776d4dc4a79db65230217ffcc2"}, diff --git a/Gemfile.lock b/Gemfile.lock index 2dfb73aa234..dc7495b058b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -766,7 +766,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - google-protobuf (3.24.4) + google-protobuf (3.25.0) googleapis-common-protos (1.4.0) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.2) @@ -1861,7 +1861,7 @@ DEPENDENCIES google-apis-serviceusage_v1 (~> 0.28.0) google-apis-sqladmin_v1beta4 (~> 0.41.0) google-cloud-storage (~> 1.44.0) - google-protobuf (~> 3.24, >= 3.24.4) + google-protobuf (~> 3.25) gpgme (~> 2.0.23) grape (~> 1.7.1) grape-entity (~> 0.10.0) diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue index 3b71e39d69b..9a7296b6b1f 100644 --- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue +++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue @@ -158,7 +158,7 @@ export default { :aria-label="$options.i18n.revokeButton" :data-confirm="modalMessage(name)" data-confirm-btn-variant="danger" - data-testid="revoke-button" + data-qa-selector="revoke_button" data-method="put" :href="revokePath" icon="remove" diff --git a/app/assets/javascripts/access_tokens/components/expires_at_field.vue b/app/assets/javascripts/access_tokens/components/expires_at_field.vue index 65206670a3c..38501d63d3a 100644 --- a/app/assets/javascripts/access_tokens/components/expires_at_field.vue +++ b/app/assets/javascripts/access_tokens/components/expires_at_field.vue @@ -68,7 +68,7 @@ export default { :input-name="inputAttrs.name" :input-id="inputAttrs.id" :placeholder="inputAttrs.placeholder" - data-testid="expiry-date-field" + data-qa-selector="expiry_date_field" /> <template #description> <template v-if="description"> diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue index f476503c091..4b51b4333aa 100644 --- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue +++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue @@ -45,7 +45,7 @@ export default { formInputGroupProps() { return { id: this.$options.tokenInputId, - 'data-testid': 'created-access-token-field', + 'data-qa-selector': 'created_access_token_field', name: this.$options.tokenInputId, }; }, @@ -110,7 +110,7 @@ export default { @[$options.EVENT_ERROR]="onError" @[$options.EVENT_SUCCESS]="onSuccess" > - <div ref="container" data-testid="access-token-section"> + <div ref="container" data-testid="access-token-section" data-qa-selector="access_token_section"> <gl-alert v-if="newToken" variant="success" diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue index 240bf005532..d3b914ea8aa 100644 --- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue +++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue @@ -115,10 +115,14 @@ export default { </gl-sprintf> </p> - <gl-card class="codes-to-print gl-my-5" data-testid="recovery-codes"> + <gl-card + class="codes-to-print gl-my-5" + data-testid="recovery-codes" + data-qa-selector="codes_content" + > <ul class="gl-m-0 gl-pl-5"> <li v-for="(code, index) in codes" :key="index"> - <span class="gl-font-monospace" data-testid="code-content">{{ code }}</span> + <span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span> </li> </ul> </gl-card> @@ -127,7 +131,7 @@ export default { <clipboard-button :title="$options.i18n.copyButton" :text="codesAsString" - data-testid="copy-button" + data-qa-selector="copy_button" @click="handleButtonClick($options.copyButtonAction)" > {{ $options.i18n.copyButton }} @@ -159,7 +163,7 @@ export default { :disabled="proceedButtonDisabled" :title="$options.i18n.proceedButton" variant="confirm" - data-testid="proceed-button" + data-qa-selector="proceed_button" data-track-action="click_button" :data-track-label="`${$options.trackingLabelPrefix}proceed_button`" >{{ $options.i18n.proceedButton }}</gl-button diff --git a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue index 7db315fda1a..424a9d3fabd 100644 --- a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue @@ -28,7 +28,12 @@ export default { </script> <template> - <gl-button :class="classes" data-testid="invite-a-group-button" @click="openModal"> + <gl-button + :class="classes" + data-qa-selector="invite_a_group_button" + data-test-id="invite-group-button" + @click="openModal" + > {{ displayText }} </gl-button> </template> diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index 7f76b7ca1ac..6efb7a6cdf1 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -4,6 +4,7 @@ import { s__ } from '~/locale'; import eventHub from '../event_hub'; import { TRIGGER_ELEMENT_BUTTON, + TRIGGER_DEFAULT_QA_SELECTOR, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, @@ -41,12 +42,18 @@ export default { required: false, default: 'button', }, + qaSelector: { + type: String, + required: false, + default: TRIGGER_DEFAULT_QA_SELECTOR, + }, }, computed: { componentAttributes() { return { class: this.classes, - 'data-testid': 'invite-members-button', + 'data-qa-selector': this.qaSelector, + 'data-test-id': 'invite-members-button', }; }, item() { diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue index 20b7096785d..18d22395104 100644 --- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue +++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue @@ -173,6 +173,7 @@ export default { variant: 'confirm', disabled: this.submitDisabled, loading: this.isLoading, + 'data-qa-selector': 'invite_button', }, }; }, @@ -310,7 +311,7 @@ export default { <gl-form-select :id="dropdownId" v-model="selectedAccessLevel" - data-testid="access-level-dropdown" + data-qa-selector="access_level_dropdown" :options="accessLevelsOptions" /> </gl-form-group> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 015cadc9993..0be04b7af35 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -102,6 +102,7 @@ export default { textInputAttrs() { return { 'data-testid': 'members-token-select-input', + 'data-qa-selector': 'members_token_select_input', id: this.inputId, }; }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 3b2840ecf11..93386e5504b 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -20,6 +20,7 @@ export const TRIGGER_ELEMENT_WITH_EMOJI = 'text-emoji'; export const TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI = 'dropdown-text-emoji'; export const TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN = 'dropdown-text'; export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal'; +export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button'; export const IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_CATEGORY = 'invite_project_members_modal'; export const IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_LABEL = 'project-members-page'; export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members'); diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index 41952a33c05..8fe822e4639 100644 --- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -8,7 +8,7 @@ const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSk if (skippable) { const button = `<div class="gl-alert-actions"> - <a class="btn gl-button btn-md btn-confirm gl-alert-action" data-testid="configure-it-later-button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a> + <a class="btn gl-button btn-md btn-confirm gl-alert-action" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a> </div>`; const flashAlert = document.querySelector('.flash-alert'); if (flashAlert) { diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index e9a67a401b8..915f6578ac3 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -42,7 +42,7 @@ export default { text: __('Delete account'), attributes: { variant: 'danger', - 'data-testid': 'confirm-delete-account-button', + 'data-qa-selector': 'confirm_delete_account_button', category: 'primary', disabled: !this.canSubmit, }, @@ -128,7 +128,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), name="password" class="form-control" type="password" - data-testid="password-confirmation-field" + data-qa-selector="password_confirmation_field" aria-labelledby="input-label" /> <input diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index c3655572e2f..891e883b6c0 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -88,7 +88,7 @@ export default { text: this.$options.i18n.editProfile, href: this.data.settings.profile_path, extraAttrs: { - 'data-testid': 'edit-profile-link', + 'data-testid': 'edit_profile_link', ...USER_MENU_TRACKING_DEFAULTS, 'data-track-label': 'user_edit_profile', }, @@ -235,7 +235,7 @@ export default { :entity-name="data.name" :src="data.avatar_url" aria-hidden="true" - data-testid="user-avatar-content" + data-testid="user_avatar_content" /> <span v-if="showNotificationDot" diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue index 75ee0e16d4e..29099bcc366 100644 --- a/app/assets/javascripts/terms/components/app.vue +++ b/app/assets/javascripts/terms/components/app.vue @@ -66,7 +66,7 @@ export default { <template> <div> - <div class="gl-relative gl-pb-0 gl-px-0" data-testid="terms-content"> + <div class="gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content"> <div class="terms-fade gl-absolute gl-left-5 gl-right-5 gl-bottom-0 gl-h-11 gl-pointer-events-none" ></div> @@ -97,7 +97,7 @@ export default { type="submit" variant="confirm" :disabled="acceptDisabled" - data-testid="accept-terms-button" + data-qa-selector="accept_terms_button" >{{ $options.i18n.accept }}</gl-button > <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue index 5019ab901fd..a1ef1f30ebb 100644 --- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue +++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue @@ -72,7 +72,7 @@ export default { attributes: { variant: 'danger', disabled: !this.isValid, - 'data-testid': 'confirm-danger-modal-button', + 'data-qa-selector': 'confirm_danger_modal_button', }, }; }, @@ -133,7 +133,8 @@ export default { id="confirm_name_input" v-model="confirmationPhrase" class="form-control" - data-testid="confirm-danger-field" + data-qa-selector="confirm_danger_field" + data-testid="confirm-danger-input" type="text" /> </gl-form-group> diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue index 344edc1082e..d97f1ae6135 100644 --- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue +++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue @@ -176,6 +176,7 @@ export default { :aria-label="toggleVisibilityLabel" :icon="toggleVisibilityIcon" data-testid="toggle-visibility-button" + data-qa-selector="toggle_visibility_button" @click.stop="handleToggleVisibilityButtonClick" /> <clipboard-button diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 28e1056092d..cd2372825ac 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -147,6 +147,8 @@ module IssuableActions finder = Issuable::DiscussionsListService.new(current_user, issuable, finder_params_for_issuable) discussion_notes = finder.execute + yield discussion_notes if block_given? + if finder.paginator.present? && finder.paginator.has_next_page? response.headers['X-Next-Page-Cursor'] = finder.paginator.cursor_for_next_page end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 77e37b8e99c..d02db2789f7 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -343,9 +343,16 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def discussions - merge_request.discussions_diffs.load_highlight + if Feature.enabled?(:only_highlight_discussions_requested, project) + super do |discussion_notes| + note_ids = discussion_notes.flat_map { |x| x.notes.collect(&:id) } + merge_request.discussions_diffs.load_highlight(diff_note_ids: note_ids) + end + else + merge_request.discussions_diffs.load_highlight - super + super + end end def export_csv diff --git a/app/graphql/resolvers/container_repository_tags_resolver.rb b/app/graphql/resolvers/container_repository_tags_resolver.rb index 55a83dd49da..bc5006ae06c 100644 --- a/app/graphql/resolvers/container_repository_tags_resolver.rb +++ b/app/graphql/resolvers/container_repository_tags_resolver.rb @@ -14,21 +14,61 @@ module Resolvers required: false, default_value: nil + alias_method :container_repository, :object + def resolve(sort:, **filters) - result = tags + if container_repository.migrated? && Feature.enabled?(:use_repository_list_tags_on_graphql, container_repository.project) + page_size = [filters[:first], filters[:last]].map(&:to_i).max + + result = container_repository.tags_page( + before: filters[:before], + last: filters[:after], + sort: map_sort_field(sort), + name: filters[:name], + page_size: page_size + ) - if filters[:name] - result = tags.filter do |tag| - tag.name.include?(filters[:name]) + Gitlab::Graphql::ExternallyPaginatedArray.new( + parse_pagination_cursor(result, :previous), + parse_pagination_cursor(result, :next), + *result[:tags] + ) + else + result = tags + + if filters[:name] + result = tags.filter do |tag| + tag.name.include?(filters[:name]) + end end - end - result = sort_tags(result, sort) if sort - result + result = sort_tags(result, sort) if sort + result + end end private + def parse_pagination_cursor(result, direction) + pagination_uri = result.dig(:pagination, direction, :uri) + + return unless pagination_uri + + query_params = CGI.parse(pagination_uri.query) + key = direction == :previous ? 'before' : 'last' + + query_params[key]&.first + end + + def map_sort_field(sort) + return unless sort + + sort_field, direction = sort.to_s.split('_') + return sort_field if direction == 'asc' + + "-#{sort_field}" + end + def sort_tags(to_be_sorted, sort) raise StandardError unless Types::ContainerRepositoryTagsSortEnum.enum.include?(sort) @@ -41,7 +81,7 @@ module Resolvers end def tags - object.tags + container_repository.tags rescue Faraday::Error raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, "Can't connect to the Container Registry. If this error persists, please review the troubleshooting documentation." end diff --git a/app/graphql/types/container_repository_details_type.rb b/app/graphql/types/container_repository_details_type.rb index 1ee9e76a1c8..b043a7c9d8d 100644 --- a/app/graphql/types/container_repository_details_type.rb +++ b/app/graphql/types/container_repository_details_type.rb @@ -13,7 +13,8 @@ module Types null: true, description: 'Tags of the container repository.', max_page_size: 20, - resolver: Resolvers::ContainerRepositoryTagsResolver + resolver: Resolvers::ContainerRepositoryTagsResolver, + connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension field :size, GraphQL::Types::Float, diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 2d6c9d4a068..fc157df3891 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -47,13 +47,13 @@ module AuthHelper provider_has_builtin_icon?(name) || provider_has_custom_icon?(name) end - def test_id_for_provider(provider) + def qa_selector_for_provider(provider) { - saml: 'saml-login-button', - openid_connect: 'oidc-login-button', - github: 'github-login-button', - gitlab: 'gitlab-oauth-login-button', - facebook: 'facebook-login-button' + saml: 'saml_login_button', + openid_connect: 'oidc_login_button', + github: 'github_login_button', + gitlab: 'gitlab_oauth_login_button', + facebook: 'facebook_login_button' }[provider.to_sym] end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 4834dca6be6..15ed517dc12 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -482,6 +482,24 @@ class ContainerRepository < ApplicationRecord raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES end + def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100) + raise ArgumentError, 'not a migrated repository' unless migrated? + + page = gitlab_api_client.tags( + self.path, + page_size: page_size, + before: before, + last: last, + sort: sort, + name: name + ) + + { + tags: transform_tags_page(page[:response_body]), + pagination: page[:pagination] + } + end + def tags_count return 0 unless manifest && manifest['tags'] @@ -636,6 +654,9 @@ class ContainerRepository < ApplicationRecord tag = ContainerRegistry::Tag.new(self, raw_tag['name']) tag.force_created_at_from_iso8601(raw_tag['created_at']) tag.updated_at = raw_tag['updated_at'] + tag.total_size = raw_tag['size_bytes'] + tag.manifest_digest = raw_tag['digest'] + tag.revision = raw_tag['config_digest'].to_s.split(':')[1] tag end end diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index 276d549006f..6d0a22c8b0a 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -2,10 +2,16 @@ module Users class CreditCardValidation < ApplicationRecord + include IgnorableColumns + RELEASE_DAY = Date.new(2021, 5, 17) self.table_name = 'user_credit_card_validations' + ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.8', remove_after: '2023-12-22' + + attr_accessor :last_digits, :network, :holder_name, :expiration_date + belongs_to :user belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id, inverse_of: :credit_card_validation diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb index 62df676db25..e0f81971944 100644 --- a/app/services/users/upsert_credit_card_validation_service.rb +++ b/app/services/users/upsert_credit_card_validation_service.rb @@ -2,41 +2,68 @@ module Users class UpsertCreditCardValidationService < BaseService + attr_reader :params + def initialize(params) @params = params.to_h.with_indifferent_access end def execute - user_id = params.fetch(:user_id) - - @params = { - user_id: user_id, - credit_card_validated_at: params.fetch(:credit_card_validated_at), - expiration_date: get_expiration_date(params), - last_digits: Integer(params.fetch(:credit_card_mask_number), 10), - network: params.fetch(:credit_card_type), - holder_name: params.fetch(:credit_card_holder_name) - } - credit_card = Users::CreditCardValidation.find_or_initialize_by_user(user_id) - credit_card.update(@params.except(:user_id)) + credit_card_params = { + credit_card_validated_at: credit_card_validated_at, + last_digits: last_digits, + holder_name: holder_name, + network: network, + expiration_date: expiration_date + } + + credit_card.update(credit_card_params) - ServiceResponse.success(message: 'CreditCardValidation was set') - rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e - ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}") + success + rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation + error rescue StandardError => e - Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s) - ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}") + Gitlab::ErrorTracking.track_exception(e) + error end private - def get_expiration_date(params) + def user_id + params.fetch(:user_id) + end + + def credit_card_validated_at + params.fetch(:credit_card_validated_at) + end + + def last_digits + Integer(params.fetch(:credit_card_mask_number), 10) + end + + def holder_name + params.fetch(:credit_card_holder_name) + end + + def network + params.fetch(:credit_card_type) + end + + def expiration_date year = params.fetch(:credit_card_expiration_year) month = params.fetch(:credit_card_expiration_month) Date.new(year, month, -1) # last day of the month end + + def success + ServiceResponse.success(message: _('Credit card validation record saved')) + end + + def error + ServiceResponse.error(message: _('Error saving credit card validation record')) + end end end diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 7dd4d119a62..35ee9a7679a 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -7,13 +7,13 @@ = f.hidden_field :reset_password_token .form-group.gl-px-5 = f.label _('New password'), for: "user_password" - = f.password_field :password, autocomplete: 'new-password', class: "form-control gl-form-input top js-password-complexity-validation", required: true, title: _('This field is required.'), data: { testid: 'password-field'} + = f.password_field :password, autocomplete: 'new-password', class: "form-control gl-form-input top js-password-complexity-validation", required: true, title: _('This field is required.'), data: { qa_selector: 'password_field'} = render_if_exists 'shared/password_requirements_list' .form-group.gl-px-5 = f.label _('Confirm new password'), for: "user_password_confirmation" - = f.password_field :password_confirmation, autocomplete: 'new-password', class: "form-control gl-form-input bottom", title: _('This field is required.'), data: { testid: 'password-confirmation-field' }, required: true + = f.password_field :password_confirmation, autocomplete: 'new-password', class: "form-control gl-form-input bottom", title: _('This field is required.'), data: { qa_selector: 'password_confirmation_field' }, required: true .clearfix.gl-px-5.gl-pb-5 - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'change-password-button' } }) do + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { qa_selector: 'change_password_button' } }) do = _('Change your password') .clearfix.prepend-top-20 diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index d9b7c986a9f..88dd4fd1721 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,10 +1,11 @@ = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'gl-p-5 gl-show-field-errors js-arkose-labs-form', aria: { live: 'assertive' }, data: { testid: 'sign-in-form' }}) do |f| .form-group = f.label :login, _('Username or primary email') - = f.text_field :login, value: @invite_email, class: 'form-control gl-form-input js-username-field', autocomplete: 'username', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { testid: 'username-field' } + = f.text_field :login, value: @invite_email, class: 'form-control gl-form-input js-username-field', autocomplete: 'username', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field', testid: 'username-field' } .form-group = f.label :password, _('Password') = f.password_field :password, class: 'form-control gl-form-input js-password', data: { id: "#{resource_name}_password", + qa_selector: 'password_field', testid: 'password-field', name: "#{resource_name}[password]" } .form-text.gl-text-right @@ -21,5 +22,5 @@ .form-group = f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { autocomplete: 'off' } - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { class: 'js-sign-in-button', data: { testid: 'sign-in-button' } }) do + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { class: 'js-sign-in-button', data: { qa_selector: 'sign_in_button', testid: 'sign-in-button' } }) do = _('Sign in') diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index db7b7a4f729..471cc053e6e 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -6,13 +6,13 @@ = gitlab_ui_form_for(provider, url: omniauth_callback_path(:user, provider), html: { class: 'gl-p-5 gl-show-field-errors', aria: { live: 'assertive' }, data: { testid: 'new_ldap_user' }}) do |f| .form-group = f.label :username, _('Username') - = f.text_field :username, name: :username, autocomplete: :username, class: 'form-control gl-form-input', title: _('This field is required.'), autofocus: 'autofocus', data: { testid: 'username-field' }, required: true + = f.text_field :username, name: :username, autocomplete: :username, class: 'form-control gl-form-input', title: _('This field is required.'), autofocus: 'autofocus', data: { qa_selector: 'username_field' }, required: true .form-group = f.label :password, _('Password') - %input.form-control.gl-form-input.js-password{ data: { id: "#{provider}_password", name: 'password', testid: 'password-field' } } + %input.form-control.gl-form-input.js-password{ data: { id: "#{provider}_password", name: 'password', qa_selector: 'password_field' } } - if render_remember_me = f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { name: :remember_me, autocomplete: 'off' } - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'sign-in-button' } }) do + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { qa_selector: 'sign_in_button' } }) do = submit_message diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 1caf0bb5893..acfb16b64cd 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -29,6 +29,6 @@ - if allow_signup? %p{ class: "gl-mt-3 #{'gl-text-center' if Feature.enabled?(:restyle_login_page, @project)}" } = _("Don't have an account yet?") - = link_to _("Register now"), new_registration_path(:user, invite_email: @invite_email), data: { testid: 'register-link' } + = link_to _("Register now"), new_registration_path(:user, invite_email: @invite_email), data: { qa_selector: 'register_link' } - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? = render 'devise/shared/omniauth_box' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 96f6f5cb095..e3457040e6c 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -5,7 +5,7 @@ = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_enabled?}" }) do |f| .form-group = f.label :otp_attempt, _('Enter verification code') - = f.text_field :otp_attempt, class: 'form-control gl-form-input', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.'), data: { testid: 'two-fa-code-field' } + = f.text_field :otp_attempt, class: 'form-control gl-form-input', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.'), data: { qa_selector: 'two_fa_code_field' } %p.form-text.text-muted.hint = _("Enter the code from your two-factor authenticator app. If you've lost your device, you can enter one of your recovery codes.") @@ -13,7 +13,7 @@ - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { testid: 'verify-code-button' } }) do + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, block: true, button_options: { data: { qa_selector: 'verify_code_button' } }) do = _("Verify code") - if @user.two_factor_webauthn_enabled? diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 45062745b77..73b9a3d5c5a 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -11,7 +11,7 @@ = _('Sign in with') - enabled_button_based_providers.each do |provider| - has_icon = provider_has_icon?(provider) - = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", data: { testid: "#{test_id_for_provider(provider)}" }, class: "btn gl-button btn-default gl-mb-2 js-oauth-login gl-w-full", form: { class: restyle_login_page_enabled ? 'gl-mb-3' : 'gl-w-full gl-mb-3' } do + = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", data: { qa_selector: "#{qa_selector_for_provider(provider)}" }, class: "btn gl-button btn-default gl-mb-2 js-oauth-login gl-w-full", form: { class: restyle_login_page_enabled ? 'gl-mb-3' : 'gl-w-full gl-mb-3' } do - if has_icon = provider_image_tag(provider) %span.gl-button-text diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml index da778120e01..b9efcaa11b4 100644 --- a/app/views/devise/shared/_signup_omniauth_provider_list.haml +++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml @@ -4,7 +4,7 @@ = _("Register with:") .gl-text-center.gl-ml-auto.gl-mr-auto - providers.each do |provider| - = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{test_id_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do + = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do - if provider_has_icon?(provider) = provider_image_tag(provider) %span.gl-button-text @@ -15,7 +15,7 @@ .gl-display-flex.gl-justify-content-between.gl-flex-wrap - providers.each do |provider| = button_to omniauth_authorize_path(:user, provider, register_omniauth_params(local_assigns)), - class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{test_id_for_provider(provider)}", + class: "btn gl-button btn-default gl-w-full gl-mb-4 js-oauth-login #{qa_selector_for_provider(provider)}", data: { provider: provider, track_action: "#{provider}_sso", track_label: tracking_label }, id: "oauth-login-#{provider}" do - if provider_has_icon?(provider) diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml index 9348a5e3451..b7ba8870df5 100644 --- a/app/views/devise/shared/_tab_single.html.haml +++ b/app/views/devise/shared/_tab_single.html.haml @@ -1,2 +1,2 @@ = gl_tabs_nav({ class: 'new-session-tabs gl-border-0' }) do - = gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1', data: { testid: 'sign-in-tab' } } + = gl_tab_link_to tab_title, '#', { item_active: true, class: 'gl-cursor-default!', tab_class: 'gl-bg-transparent!', tabindex: '-1', data: { qa_selector: 'sign_in_tab', testid: 'sign-in-tab' } } diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index e6bc38ba6dd..76c4cf41a2d 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -8,13 +8,13 @@ = render_if_exists "devise/shared/kerberos_tab" - ldap_servers.each_with_index do |server, i| %li.nav-item - = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', testid: 'ldap-tab' }, role: 'tab' + = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', qa_selector: 'ldap_tab', testid: 'ldap-tab' }, role: 'tab' = render_if_exists 'devise/shared/tab_smartcard' - if show_password_form %li.nav-item - = link_to _('Standard'), '#login-pane', class: 'nav-link', data: { toggle: 'tab', testid: 'standard-tab' }, role: 'tab' + = link_to _('Standard'), '#login-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'standard_tab' }, role: 'tab' - if render_signup_link && allow_signup? %li.nav-item - = link_to _('Register'), '#register-pane', class: 'nav-link', data: { toggle: 'tab', testid: 'register-tab' }, role: 'tab' + = link_to _('Register'), '#register-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'register_tab' }, role: 'tab' diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 116732cd98d..fd5088e04b0 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -51,5 +51,5 @@ = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method = render Pajamas::ButtonComponent.new(type: :submit, variant: :danger, - button_options: { id: 'commit-changes', class: 'gl-ml-3', testid: 'authorization_button'}) do + button_options: { id: 'commit-changes', class: 'gl-ml-3', qa_selector: 'authorization_button'}) do = _("Authorize") diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 7794e3c9853..4e9ae7c7fd8 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -3,7 +3,7 @@ !!! 5 %html.html-devise-layout{ lang: I18n.locale } = render "layouts/head", { startup_filename: 'signin' } - %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, testid: 'login-page' } } + %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{user_application_theme} #{client_class_list}", data: { page: body_data_page, qa_selector: 'login_page' } } = header_message = render "layouts/init_client_detection_flags" - if Feature.enabled?(:restyle_login_page, @project) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index fb875c67b6d..993094c6889 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -110,7 +110,7 @@ - if header_link?(:user_dropdown) %li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { testid: 'user-dropdown' }, class: ('mr-0' if has_impersonation_link) } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown", track_label: "profile_dropdown", track_action: "click_dropdown", track_property: "navigation_top" } do - = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar', avatar_options: { data: { testid: 'user-avatar-content' } }) + = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar', avatar_options: { data: { testid: 'user_avatar_content' } }) = render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group = sprite_icon('chevron-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index 621c0579226..32f00a4c0c6 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -25,6 +25,6 @@ .gl-text-right.gl-line-height-normal .gl-font-weight-bold= current_user.name .gl-text-gray-700 @#{current_user.username} - = render Pajamas::AvatarComponent.new(current_user, size: 32, avatar_options: { data: { testid: 'user-avatar-content' } }) + = render Pajamas::AvatarComponent.new(current_user, size: 32, avatar_options: { data: { qa_selector: 'user_avatar_content' } }) - c.with_body do = yield diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 75dba925328..799dfaae8c5 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -30,7 +30,7 @@ = render Pajamas::ButtonComponent.new(variant: :confirm, href: profile_two_factor_auth_path) do = _('Manage two-factor authentication') - else - = render Pajamas::ButtonComponent.new(variant: :confirm, href: profile_two_factor_auth_path, button_options: { data: { testid: 'enable-2fa-button' }}) do + = render Pajamas::ButtonComponent.new(variant: :confirm, href: profile_two_factor_auth_path, button_options: { data: { qa_selector: 'enable_2fa_button' }}) do = _('Enable two-factor authentication') - if display_providers_on_profile? @@ -76,7 +76,7 @@ = render 'users/deletion_guidance', user: current_user -# Delete button here - = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'delete-account-button', disabled: true, data: { testid: 'delete-account-button' }}) do + = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'delete-account-button', disabled: true, data: { qa_selector: 'delete_account_button' }}) do = s_('Profiles|Delete account') #delete-account-modal{ data: { action_url: user_registration_path, diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 25badbcdb16..ff0b31da022 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -29,7 +29,7 @@ = _("To add the entry manually, provide the following details to the application on your phone.") %p.gl-mt-0.gl-mb-0 = _('Account: %{account}') % { account: @account_string } - %p.gl-mt-0.gl-mb-0{ data: { testid: 'otp-secret-content' } } + %p.gl-mt-0.gl-mb-0{ data: { qa_selector: 'otp_secret_content' } } = _('Key:') %code.two-factor-secret= current_user.otp_secret.scan(/.{4}/).join(' ') %p.gl-mb-0.two-factor-new-manual-content @@ -46,14 +46,14 @@ - if current_password_required? .form-group = label_tag :current_password, _('Current password'), class: 'label-bold' - = password_field_tag :current_password, nil, autocomplete: 'current-password', required: true, class: 'form-control gl-form-input', data: { testid: 'current-password-field' } + = password_field_tag :current_password, nil, autocomplete: 'current-password', required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' } %p.form-text.text-muted = _('Your current password is required to register a two-factor authenticator app.') .form-group = label_tag :pin_code, _('Enter verification code'), class: "label-bold" - = text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { testid: 'pin-code-field' } + = text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' } .gl-mt-3 - = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { data: { testid: 'register-2fa-app-button' } }) do + = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { data: { qa_selector: 'register_2fa_app_button' } }) do = _('Register with two-factor app') %hr diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index 79c4bfca630..3bf85da83b1 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -15,7 +15,7 @@ .form-group = f.label :name, s_('AccessTokens|Token name'), class: 'label-bold' - = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', required: true, data: { testid: 'access-token-name-field' }, :'aria-describedby' => 'access_token_help_text' + = f.text_field :name, class: 'form-control gl-form-input gl-form-input-xl', required: true, data: { qa_selector: 'access_token_name_field' }, :'aria-describedby' => 'access_token_help_text' %span.form-text.text-muted#access_token_help_text - if resource - resource_type = resource.is_a?(Group) ? "group" : "project" @@ -42,6 +42,6 @@ = render 'shared/tokens/scopes_form', prefix: prefix, description_prefix: description_prefix, token: token, scopes: scopes, f: f .gl-mt-3 - = f.submit s_('AccessTokens|Create %{type}') % { type: type }, data: { testid: 'create-token-button' }, pajamas_button: true + = f.submit s_('AccessTokens|Create %{type}') % { type: type }, data: { qa_selector: 'create_token_button' }, pajamas_button: true = render Pajamas::ButtonComponent.new(button_options: { type: 'reset', class: 'gl-ml-2 js-toggle-button' }) do = _('Cancel') diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml index 44d261b84f1..bb7e0d774cc 100644 --- a/app/views/shared/deploy_tokens/_form.html.haml +++ b/app/views/shared/deploy_tokens/_form.html.haml @@ -6,12 +6,12 @@ .form-group = f.label :name, class: 'label-bold' - = f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'deploy-token-name-field' }, required: true + = f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true .text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.') .form-group = f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold' - = f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-token-expires-at-field' }, value: f.object.expires_at + = f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at .text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.') .form-group @@ -22,15 +22,15 @@ .form-group = f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold' - = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { testid: 'deploy-token-read-repository-checkbox' } } + = f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_repository_checkbox' } } - if container_registry_enabled?(group_or_project) - = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-read-registry-checkbox' } } - = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-write-registry-checkbox' } } + = f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_registry_checkbox' } } + = f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_registry_checkbox' } } - if packages_registry_enabled?(group_or_project) - = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-read-package-registry-checkbox' } } - = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-write-package-registry-checkbox' } } + = f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } } + = f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } } .gl-mt-3 - = f.submit s_('DeployTokens|Create deploy token'), data: { testid: 'create-deploy-token-button' }, pajamas_button: true + = f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true diff --git a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml index 2bc2e6c5b81..30917ee6fff 100644 --- a/app/views/shared/deploy_tokens/_new_deploy_token.html.haml +++ b/app/views/shared/deploy_tokens/_new_deploy_token.html.haml @@ -1,11 +1,11 @@ -.created-deploy-token-container.info-well{ data: { testid: 'created-deploy-token-container' } } +.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } } .well-segment %h5.gl-mt-0 = s_('DeployTokens|Your new Deploy Token username') .form-group .input-group - = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-user-field' } + = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' } .input-group-append = deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left') %span.deploy-token-help-block.gl-mt-2.text-success @@ -15,7 +15,7 @@ .form-group .input-group - = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-field' } + = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' } .input-group-append = deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left') %span.deploy-token-help-block.gl-mt-2.text-danger diff --git a/config/feature_flags/development/only_highlight_discussions_requested.yml b/config/feature_flags/development/only_highlight_discussions_requested.yml new file mode 100644 index 00000000000..8dfb93c33e0 --- /dev/null +++ b/config/feature_flags/development/only_highlight_discussions_requested.yml @@ -0,0 +1,8 @@ +--- +name: only_highlight_discussions_requested +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135096 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429489 +milestone: '16.6' +type: development +group: group::code review +default_enabled: false diff --git a/config/feature_flags/development/use_repository_list_tags_on_graphql.yml b/config/feature_flags/development/use_repository_list_tags_on_graphql.yml new file mode 100644 index 00000000000..926d952e6f9 --- /dev/null +++ b/config/feature_flags/development/use_repository_list_tags_on_graphql.yml @@ -0,0 +1,8 @@ +--- +name: use_repository_list_tags_on_graphql +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132716 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426358 +milestone: '16.5' +type: development +group: group::container registry +default_enabled: false diff --git a/doc/administration/cicd.md b/doc/administration/cicd.md index 47cbd7d1aec..9c69a7cbb65 100644 --- a/doc/administration/cicd.md +++ b/doc/administration/cicd.md @@ -93,14 +93,96 @@ To change the frequency of the pipeline schedule worker: For example, to set the maximum frequency of pipelines to twice a day, set `pipeline_schedule_worker_cron` to a cron value of `0 */12 * * *` (`00:00` and `12:00` every day). -<!-- ## Troubleshooting +## Disaster recovery -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. +You can disable some important but computationally expensive parts of the application +to relieve stress on the database during ongoing downtime. -Each scenario can be a third-level heading, for example `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +### Disable fair scheduling on shared runners + +When clearing a large backlog of jobs, you can temporarily enable the `ci_queueing_disaster_recovery_disable_fair_scheduling` +[feature flag](../administration/feature_flags.md). This flag disables fair scheduling +on shared runners, which reduces system resource usage on the `jobs/request` endpoint. + +When enabled, jobs are processed in the order they were put in the system, instead of +balanced across many projects. + +### Disable compute quota enforcement + +To disable the enforcement of [compute quotas](../ci/pipelines/cicd_minutes.md) on shared runners, you can temporarily +enable the `ci_queueing_disaster_recovery_disable_quota` [feature flag](../administration/feature_flags.md). +This flag reduces system resource usage on the `jobs/request` endpoint. + +When enabled, jobs created in the last hour can run in projects which are out of quota. +Earlier jobs are already canceled by a periodic background worker (`StuckCiJobsWorker`). + +## CI/CD troubleshooting Rails console commands + +The following commands are run in the [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session). + +WARNING: +Any command that changes data directly could be damaging if not run correctly, or under the right conditions. +We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case. + +### Cancel stuck pending pipelines + +```ruby +project = Project.find_by_full_path('<project_path>') +Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count +Ci::Pipeline.where(project_id: project.id).where(status: 'pending').each {|p| p.cancel if p.stuck?} +Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count +``` + +### Try merge request integration + +```ruby +project = Project.find_by_full_path('<project_path>') +mr = project.merge_requests.find_by(iid: <merge_request_iid>) +mr.project.try(:ci_integration) +``` + +### Validate the `.gitlab-ci.yml` file + +```ruby +project = Project.find_by_full_path('<project_path>') +content = p.ci_config_for(project.repository.root_ref_sha) +Gitlab::Ci::Lint.new(project: project, current_user: User.first).validate(content) +``` + +### Disable AutoDevOps on Existing Projects + +```ruby +Project.all.each do |p| + p.auto_devops_attributes={"enabled"=>"0"} + p.save +end +``` + +### Obtain runners registration token + +```ruby +Gitlab::CurrentSettings.current_application_settings.runners_registration_token +``` + +### Seed runners registration token + +```ruby +appSetting = Gitlab::CurrentSettings.current_application_settings +appSetting.set_runners_registration_token('<new-runners-registration-token>') +appSetting.save! +``` + +### Run pipeline schedules manually + +You can run pipeline schedules manually through the Rails console to reveal any errors that are usually not visible. + +```ruby +# schedule_id can be obtained from Edit Pipeline Schedule page +schedule = Ci::PipelineSchedule.find_by(id: <schedule_id>) + +# Select the user that you want to run the schedule for +user = User.find_by_username('<username>') + +# Run the schedule +ps = Ci::CreatePipelineService.new(schedule.project, user, ref: schedule.ref).execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule) +``` diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md index 44aa3d648ad..e2fc425b536 100644 --- a/doc/administration/reference_architectures/index.md +++ b/doc/administration/reference_architectures/index.md @@ -191,13 +191,22 @@ Before implementing a reference architecture, refer to the following requirement These reference architectures were built and tested on Google Cloud Platform (GCP) using the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms) CPU platform as a lowest common denominator baseline ([Sysbench benchmark](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks)). +Newer, similarly-sized CPUs are supported and may have improved performance as a result. -Newer, similarly-sized CPUs are supported and may have improved performance as a result. For Linux package environments, -ARM-based equivalents are also supported. +ARM CPUs are supported for Linux package environments as well as for any [Cloud Provider services](#cloud-provider-services) where applicable. NOTE: Any "burstable" instance types are not recommended due to inconsistent performance. +### Supported disk types + +As a general guidance, most standard disk types are expected to work for GitLab, but be aware of the following specific call outs: + +- [Gitaly](../gitaly/index.md#disk-requirements) requires at least 8,000 input/output operations per second (IOPS) for read operations, and 2,000 IOPS for write operations. +- We don't recommend the use of any disk types that are "burstable" due to inconsistent performance. + +Outside the above standard, disk types are expected to work for GitLab and the choice of each depends on your specific requirements around areas, such as durability or costs. + ### Supported infrastructure As a general guidance, GitLab should run on most infrastructure such as reputable Cloud Providers (AWS, GCP, Azure) and @@ -356,6 +365,12 @@ If you choose to use a third party external service: Redis is primarily single threaded. For the 10,000 user and above Reference Architectures, separate out the instances as specified into Cache and Persistent data to achieve optimum performance at this scale. +### Recommendation notes for Object Storage + +GitLab has been tested against [various Object Storage providers](../object_storage.md#supported-object-storage-providers) that are expected to work. + +As a general guidance, it's recommended to use a reputable solution that has full S3 compatibility. + #### Unsupported database services Several database cloud provider services are known not to support the above or have been found to have other issues and aren't recommended: @@ -663,8 +678,8 @@ Most setups would only need vertical scaling, but there are some specific areas Conversely, if you have robust metrics in place that show the environment is over-provisioned, you can apply the same process for scaling downwards. You should take an iterative approach when scaling downwards, however, to ensure there are no issues. -### How to monitor your environment +### Monitoring + +There are numerous options available to monitor your infrastructure, as well as [GitLab itself](../monitoring/index.md), and you should refer to your chosen monitoring solution's documentation for more information. -To monitor your GitLab environment, you can use the tools -[bundled with GitLab](../monitoring/index.md), but it's also possible to use third-party -options if desired. +Of note, the GitLab application is bundled with [Prometheus as well as various Prometheus compatible exporters](../monitoring/prometheus/index.md) that could be hooked into your solution. diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md index 9432836c22b..01c75c32366 100644 --- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md +++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md @@ -46,11 +46,11 @@ This content has been moved to [Troubleshooting Repository mirroring](../../user ## CI -This content has been moved to [Troubleshooting CI/CD](../../ci/troubleshooting.md). +This content has been moved to [Troubleshooting CI/CD](../cicd.md#cicd-troubleshooting-rails-console-commands). ## License -This content has been moved to [Activate GitLab EE with a license file or key](../../administration/license_file.md). +This content has been moved to [Activate GitLab EE with a license file or key](../license_file.md). ## Registry diff --git a/doc/ci/debugging.md b/doc/ci/debugging.md new file mode 100644 index 00000000000..bc4482a43e2 --- /dev/null +++ b/doc/ci/debugging.md @@ -0,0 +1,280 @@ +--- +stage: Verify +group: Pipeline Authoring +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +type: reference +--- + +# Debugging CI/CD pipelines **(FREE ALL)** + +GitLab provides several tools to help make it easier to debug your CI/CD configuration. + +If you are unable to resolve pipeline issues, you can get help from: + +- The [GitLab community forum](https://forum.gitlab.com/) +- GitLab [Support](https://about.gitlab.com/support/) + +## Verify syntax + +An early source of problems can be incorrect syntax. The pipeline shows a `yaml invalid` +badge and does not start running if any syntax or formatting problems are found. + +### Edit `.gitlab-ci.yml` with the pipeline editor + +The [pipeline editor](pipeline_editor/index.md) is the recommended editing +experience (rather than the single file editor or the Web IDE). It includes: + +- Code completion suggestions that ensure you are only using accepted keywords. +- Automatic syntax highlighting and validation. +- The [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration), + a graphical representation of your `.gitlab-ci.yml` file. + +### Edit `.gitlab-ci.yml` locally + +If you prefer to edit your pipeline configuration locally, you can use the +GitLab CI/CD schema in your editor to verify basic syntax issues. Any +[editor with Schemastore support](https://www.schemastore.org/json/#editors) uses +the GitLab CI/CD schema by default. + +If you need to link to the schema directly, use this URL: + +```plaintext +https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/editor/schema/ci.json +``` + +To see the full list of custom tags covered by the CI/CD schema, check the +latest version of the schema. + +### Verify syntax with CI Lint tool + +You can use the [CI Lint tool](lint.md) to verify that the syntax of a CI/CD configuration +snippet is correct. Paste in full `.gitlab-ci.yml` files or individual job configurations, +to verify the basic syntax. + +When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint +tool to [simulate the creation of a full pipeline](lint.md#simulate-a-pipeline). +It does deeper verification of the configuration syntax. + +## Verify variables + +A key part of troubleshooting CI/CD is to verify which variables are present in a +pipeline, and what their values are. A lot of pipeline configuration is dependent +on variables, and verifying them is one of the fastest ways to find the source of +a problem. + +[Export the full list of variables](variables/index.md#list-all-variables) +available in each problematic job. Check if the variables you expect are present, +and check if their values are what you expect. + +## Job configuration issues + +A lot of common pipeline issues can be fixed by analyzing the behavior of the `rules` +or `only/except` configuration used to [control when jobs are added to a pipeline](jobs/job_control.md). +You shouldn't use these two configurations in the same pipeline, as they behave differently. +It's hard to predict how a pipeline runs with this mixed behavior. `rules` is the preferred +choice for controlling jobs, as `only` and `except` are no longer being actively developed. + +If your `rules` or `only/except` configuration makes use of [predefined variables](variables/predefined_variables.md) +like `CI_PIPELINE_SOURCE`, `CI_MERGE_REQUEST_ID`, you should [verify them](#verify-variables) +as the first troubleshooting step. + +### Jobs or pipelines don't run when expected + +The `rules` or `only/except` keywords are what determine whether or not a job is +added to a pipeline. If a pipeline runs, but a job is not added to the pipeline, +it's usually due to `rules` or `only/except` configuration issues. + +If a pipeline does not seem to run at all, with no error message, it may also be +due to `rules` or `only/except` configuration, or the `workflow: rules` keyword. + +If you are converting from `only/except` to the `rules` keyword, you should check +the [`rules` configuration details](yaml/index.md#rules) carefully. The behavior +of `only/except` and `rules` is different and can cause unexpected behavior when migrating +between the two. + +The [common `if` clauses for `rules`](jobs/job_control.md#common-if-clauses-for-rules) +can be very helpful for examples of how to write rules that behave the way you expect. + +### A job with the `changes` keyword runs unexpectedly + +A common reason a job is added to a pipeline unexpectedly is because the `changes` +keyword always evaluates to true in certain cases. For example, `changes` is always +true in certain pipeline types, including scheduled pipelines and pipelines for tags. + +The `changes` keyword is used in combination with [`only/except`](yaml/index.md#onlychanges--exceptchanges) +or [`rules`](yaml/index.md#ruleschanges). It's recommended to only use `changes` with +`if` sections in `rules` or `only/except` configuration that ensures the job is only added to +branch pipelines or merge request pipelines. + +### Two pipelines run at the same time + +Two pipelines can run when pushing a commit to a branch that has an open merge request +associated with it. Usually one pipeline is a merge request pipeline, and the other +is a branch pipeline. + +This situation is usually caused by the `rules` configuration, and there are several ways to +[prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines). + +### No pipeline or the wrong type of pipeline runs + +Before a pipeline can run, GitLab evaluates all the jobs in the configuration and tries +to add them to all available pipeline types. A pipeline does not run if no jobs are added +to it at the end of the evaluation. + +If a pipeline did not run, it's likely that all the jobs had `rules` or `only/except` that +blocked them from being added to the pipeline. + +If the wrong pipeline type ran, then the `rules` or `only/except` configuration should +be checked to make sure the jobs are added to the correct pipeline type. For +example, if a merge request pipeline did not run, the jobs may have been added to +a branch pipeline instead. + +It's also possible that your [`workflow: rules`](yaml/index.md#workflow) configuration +blocked the pipeline, or allowed the wrong pipeline type. + +### Pipeline with many jobs fails to start + +A Pipeline that has more jobs than the instance's defined [CI/CD limits](../administration/settings/continuous_integration.md#set-cicd-limits) +fails to start. + +To reduce the number of jobs in a single pipeline, you can split your `.gitlab-ci.yml` +configuration into more independent [parent-child pipelines](../ci/pipelines/pipeline_architectures.md#parent-child-pipelines). + +## Pipeline warnings + +Pipeline configuration warnings are shown when you: + +- [Validate configuration with the CI Lint tool](yaml/index.md). +- [Manually run a pipeline](pipelines/index.md#run-a-pipeline-manually). + +### `Job may allow multiple pipelines to run for a single action` warning + +When you use [`rules`](yaml/index.md#rules) with a `when` clause without an `if` +clause, multiple pipelines may run. Usually this occurs when you push a commit to +a branch that has an open merge request associated with it. + +To [prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines), use +[`workflow: rules`](yaml/index.md#workflow) or rewrite your rules to control +which pipelines can run. + +## Troubleshooting + +For help with a specific area, see: + +- [Caching](caching/index.md#troubleshooting). +- [CI/CD job tokens](jobs/ci_job_token.md). +- [Container Registry](../user/packages/container_registry/troubleshoot_container_registry.md). +- [Docker](docker/using_docker_build.md#troubleshooting). +- [Downstream pipelines](pipelines/downstream_pipelines.md#troubleshooting). +- [Environments](environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time). +- [GitLab Runner](https://docs.gitlab.com/runner/faq/). +- [ID tokens](secrets/id_token_authentication.md#troubleshooting). +- [Jobs](jobs/index.md#troubleshooting). +- [Job control](jobs/job_control.md). +- [Job artifacts](jobs/job_artifacts_troubleshooting.md). +- [Merge request pipelines](pipelines/merge_request_pipelines.md#troubleshooting), + [merged results pipelines](pipelines/merged_results_pipelines.md#troubleshooting), + and [Merge trains](pipelines/merge_trains.md#troubleshooting). +- [Pipeline editor](pipeline_editor/index.md#troubleshooting). +- [Variables](variables/index.md#troubleshooting). +- [YAML `includes` keyword](yaml/includes.md#troubleshooting). +- [YAML `script` keyword](yaml/script.md#troubleshooting). + +Otherwise, review the following troubleshooting sections for known status messages +and error messages. + +### `A CI/CD pipeline must run and be successful before merge` message + +This message is shown if the [**Pipelines must succeed**](../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge) +setting is enabled in the project and a pipeline has not yet run successfully. +This also applies if the pipeline has not been created yet, or if you are waiting +for an external CI service. + +If you don't use pipelines for your project, then you should disable **Pipelines must succeed** +so you can accept merge requests. + +### `Checking ability to merge automatically` message + +If your merge request is stuck with a `Checking ability to merge automatically` +message that does not disappear after a few minutes, you can try one of these workarounds: + +- Refresh the merge request page. +- Close & Re-open the merge request. +- Rebase the merge request with the `/rebase` [quick action](../user/project/quick_actions.md). +- If you have already confirmed the merge request is ready to be merged, you can merge + it with the `/merge` quick action. + +This issue is [resolved](https://gitlab.com/gitlab-org/gitlab/-/issues/229352) in GitLab 15.5. + +### `Checking pipeline status` message + +This message displays when the merge request does not yet have a pipeline associated with the +latest commit. This might be because: + +- GitLab hasn't finished creating the pipeline yet. +- You are using an external CI service and GitLab hasn't heard back from the service yet. +- You are not using CI/CD pipelines in your project. +- You are using CI/CD pipelines in your project, but your configuration prevented a pipeline from running on the source branch for your merge request. +- The latest pipeline was deleted (this is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214323)). +- The source branch of the merge request is on a private fork. + +After the pipeline is created, the message updates with the pipeline status. + +### `Project <group/project> not found or access denied` message + +This message is shown if configuration is added with [`include`](yaml/index.md#include) and either: + +- The configuration refers to a project that can't be found. +- The user that is running the pipeline is unable to access any included projects. + +To resolve this, check that: + +- The path of the project is in the format `my-group/my-project` and does not include + any folders in the repository. +- The user running the pipeline is a [member of the projects](../user/project/members/index.md#add-users-to-a-project) + that contain the included files. Users must also have the [permission](../user/permissions.md#job-permissions) + to run CI/CD jobs in the same projects. + +### `The parsed YAML is too big` message + +This message displays when the YAML configuration is too large or nested too deeply. +YAML files with a large number of includes, and thousands of lines overall, are +more likely to hit this memory limit. For example, a YAML file that is 200 kb is +likely to hit the default memory limit. + +To reduce the configuration size, you can: + +- Check the length of the expanded CI/CD configuration in the pipeline editor's + [Full configuration](pipeline_editor/index.md#view-full-configuration) tab. Look for + duplicated configuration that can be removed or simplified. +- Move long or repeated `script` sections into standalone scripts in the project. +- Use [parent and child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines) to move some + work to jobs in an independent child pipeline. + +On a self-managed instance, you can [increase the size limits](../administration/instance_limits.md#maximum-size-and-depth-of-cicd-configuration-yaml-files). + +### `500` error when editing the `.gitlab-ci.yml` file + +A [loop of included configuration files](pipeline_editor/index.md#configuration-validation-currently-not-available-message) +can cause a `500` error when editing the `.gitlab-ci.yml` file with the [web editor](../user/project/repository/web_editor.md). + +Ensure that included configuration files do not create a loop of references to each other. + +### `Failed to pull image` message + +> **Allow access to this project with a CI_JOB_TOKEN** setting [renamed to **Limit access _to_ this project**](https://gitlab.com/gitlab-org/gitlab/-/issues/411406) in GitLab 16.3. + +When a runner tries to pull an image from a private project, the job could fail with the following error: + +```shell +WARNING: Failed to pull image with policy "always": Error response from daemon: pull access denied for registry.example.com/path/to/project, repository does not exist or may require 'docker login': denied: requested access to the resource is denied +``` + +This error can happen if the following are both true: + +- The **Limit access _to_ this project** option is enabled in the private project + hosting the image. +- The job attempting to fetch the image is running for a project that is not listed in + the private project's allowlist. + +The recommended solution is to [add your project to the private project's job token scope allowlist](jobs/ci_job_token.md#add-a-project-to-the-job-token-scope-allowlist). diff --git a/doc/ci/jobs/index.md b/doc/ci/jobs/index.md index 90a64ea7569..761e9e6dd66 100644 --- a/doc/ci/jobs/index.md +++ b/doc/ci/jobs/index.md @@ -397,3 +397,67 @@ The behavior of deployment jobs can be controlled with [deployment safety](../environments/deployment_safety.md) settings like [preventing outdated deployment jobs](../environments/deployment_safety.md#prevent-outdated-deployment-jobs) and [ensuring only one deployment job runs at a time](../environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time). + +## Troubleshooting + +### Job log slow to update + +When you visit the job log page for a running job, there could be a delay of up to +60 seconds before a log update. The default refresh time is 60 seconds, but after +the log is viewed in the UI one time, log updates should occur every 3 seconds. + +### `get_sources` job section fails because of an HTTP/2 problem + +Sometimes, jobs fail with the following cURL error: + +```plaintext +++ git -c 'http.userAgent=gitlab-runner <version>' fetch origin +refs/pipelines/<id>:refs/pipelines/<id> ... +error: RPC failed; curl 16 HTTP/2 send again with decreased length +fatal: ... +``` + +You can work around this problem by configuring Git and `libcurl` to +[use HTTP/1.1](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpversion). +The configuration can be added to: + +- A job's [`pre_get_sources_script`](../yaml/index.md#hookspre_get_sources_script): + + ```yaml + job_name: + hooks: + pre_get_sources_script: + - git config --global http.version "HTTP/1.1" + ``` + +- The [runner's `config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html) + with [Git configuration environment variables](https://git-scm.com/docs/git-config#ENVIRONMENT): + + ```toml + [[runners]] + ... + environment = [ + "GIT_CONFIG_COUNT=1", + "GIT_CONFIG_KEY_0=http.version", + "GIT_CONFIG_VALUE_0=HTTP/1.1" + ] + ``` + +### Job using `resource_group` gets stuck **(FREE SELF)** + +If a job using [`resource_group`](../yaml/index.md#resource_group) gets stuck, a +GitLab administrator can try run the following commands from the [rails console](../../administration/operations/rails_console.md#starting-a-rails-console-session): + +```ruby +# find resource group by name +resource_group = Project.find_by_full_path('...').resource_groups.find_by(key: 'the-group-name') +busy_resources = resource_group.resources.where('build_id IS NOT NULL') + +# identify which builds are occupying the resource +# (I think it should be 1 as of today) +busy_resources.pluck(:build_id) + +# it's good to check why this build is holding the resource. +# Is it stuck? Has it been forcefully dropped by the system? +# free up busy resources +busy_resources.update_all(build_id: nil) +``` diff --git a/doc/ci/jobs/job_control.md b/doc/ci/jobs/job_control.md index 418991ab5f7..0c8e4fc593f 100644 --- a/doc/ci/jobs/job_control.md +++ b/doc/ci/jobs/job_control.md @@ -174,8 +174,7 @@ multiple pipelines. You don't have to explicitly configure rules for multiple ty of pipeline to trigger them accidentally. Some configurations that have the potential to cause duplicate pipelines cause a -[pipeline warning](../troubleshooting.md#pipeline-warnings) to be displayed. -[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219431) in GitLab 13.3. +[pipeline warning](../debugging.md#pipeline-warnings) to be displayed. For example: @@ -209,7 +208,7 @@ To avoid duplicate pipelines, you can: You can also avoid duplicate pipelines by changing the job rules to avoid either push (branch) pipelines or merge request pipelines. However, if you use a `- when: always` rule without -`workflow: rules`, GitLab still displays a [pipeline warning](../troubleshooting.md#pipeline-warnings). +`workflow: rules`, GitLab still displays a [pipeline warning](../debugging.md#pipeline-warnings). For example, the following does not trigger double pipelines, but is not recommended without `workflow: rules`: @@ -1194,3 +1193,20 @@ To run protected manual jobs: - Add the administrator as a direct member of the private project (any role) - [Impersonate a user](../../administration/admin_area.md#user-impersonation) who is a direct member of the project. + +### A CI/CD job does not use newer configuration when run again + +The configuration for a pipeline is only fetched when the pipeline is created. +When you rerun a job, uses the same configuration each time. If you update configuration files, +including separate files added with [`include`](../yaml/index.md#include), you must +start a new pipeline to use the new configuration. + +### `Job may allow multiple pipelines to run for a single action` warning + +When you use [`rules`](../yaml/index.md#rules) with a `when` clause without an `if` +clause, multiple pipelines may run. Usually this occurs when you push a commit to +a branch that has an open merge request associated with it. + +To [prevent duplicate pipelines](#avoid-duplicate-pipelines), use +[`workflow: rules`](../yaml/index.md#workflow) or rewrite your rules to control +which pipelines can run. diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md index 37febfd90ee..fb1c19d8770 100644 --- a/doc/ci/pipelines/merge_request_pipelines.md +++ b/doc/ci/pipelines/merge_request_pipelines.md @@ -262,3 +262,25 @@ Some possible reasons for this error message: If **Run pipeline** is available, but the project does not have merge request pipelines enabled, do not use this option. You can push a commit or rebase the branch to trigger new branch pipelines. + +### `Merge blocked: pipeline must succeed. Push a new commit that fixes the failure` message + +This message is shown if the merge request pipeline, [merged results pipeline](merged_results_pipelines.md), +or [merge train pipeline](merge_trains.md) has failed or been canceled. +This does not happen when a branch pipeline fails. + +If a merge request pipeline or merged result pipeline was canceled or failed, you can: + +- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request. +- [Retry only the jobs that failed](index.md#view-pipelines). If you re-run the entire pipeline, this is not necessary. +- Push a new commit to fix the failure. + +If the merge train pipeline has failed, you can: + +- Check the failure and determine if you can use the [`/merge` quick action](../../user/project/quick_actions.md) to immediately add the merge request to the train again. +- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request, then add the merge request to the train again. +- Push a commit to fix the failure, then add the merge request to the train again. + +If the merge train pipeline was canceled before the merge request was merged, without a failure, you can: + +- Add it to the train again. diff --git a/doc/ci/pipelines/merged_results_pipelines.md b/doc/ci/pipelines/merged_results_pipelines.md index e4f739e8242..afe7a450370 100644 --- a/doc/ci/pipelines/merged_results_pipelines.md +++ b/doc/ci/pipelines/merged_results_pipelines.md @@ -61,19 +61,6 @@ Upgrade to 13.8 or later, or make sure the `:merge_ref_auto_sync` [feature flag is enabled](../../administration/feature_flags.md#check-if-a-feature-flag-is-enabled) on your GitLab instance. -### Pipelines fail intermittently with a `fatal: reference is not a tree:` error - -Merged results pipelines run on a merge ref for a merge request -(`refs/merge-requests/<iid>/merge`), so the Git reference could be overwritten at an -unexpected time. - -For example, when a source or target branch is advanced, the pipeline fails with -the `fatal: reference is not a tree:` error, which indicates that the checkout-SHA -is not found in the merge ref. - -This behavior was improved in GitLab 12.4 by introducing [persistent pipeline refs](../troubleshooting.md#fatal-reference-is-not-a-tree-error). -Upgrade to GitLab 12.4 or later to resolve the problem. - ### Successful merged results pipeline overrides a failed branch pipeline A failed branch pipeline is sometimes ignored when the diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md index b09b85f33a0..77ee6b11d92 100644 --- a/doc/ci/troubleshooting.md +++ b/doc/ci/troubleshooting.md @@ -1,555 +1,11 @@ --- -stage: Verify -group: Pipeline Authoring -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments -type: reference +redirect_to: 'debugging.md' +remove_date: '2024-02-01' --- -# Troubleshooting CI/CD **(FREE ALL)** +This document was moved to [another location](debugging.md). -GitLab provides several tools to help make troubleshooting your pipelines easier. - -This guide also lists common issues and possible solutions. - -## Verify syntax - -An early source of problems can be incorrect syntax. The pipeline shows a `yaml invalid` -badge and does not start running if any syntax or formatting problems are found. - -### Edit `.gitlab-ci.yml` with the pipeline editor - -The [pipeline editor](pipeline_editor/index.md) is the recommended editing -experience (rather than the single file editor or the Web IDE). It includes: - -- Code completion suggestions that ensure you are only using accepted keywords. -- Automatic syntax highlighting and validation. -- The [CI/CD configuration visualization](pipeline_editor/index.md#visualize-ci-configuration), - a graphical representation of your `.gitlab-ci.yml` file. - -### Edit `.gitlab-ci.yml` locally - -If you prefer to edit your pipeline configuration locally, you can use the -GitLab CI/CD schema in your editor to verify basic syntax issues. Any -[editor with Schemastore support](https://www.schemastore.org/json/#editors) uses -the GitLab CI/CD schema by default. - -If you need to link to the schema directly, it -is at: - -```plaintext -https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/editor/schema/ci.json -``` - -To see the full list of custom tags covered by the CI/CD schema, check the -latest version of the schema. - -### Verify syntax with CI Lint tool - -The [CI Lint tool](lint.md) is a simple way to ensure the syntax of a CI/CD configuration -file is correct. Paste in full `.gitlab-ci.yml` files or individual jobs configuration, -to verify the basic syntax. - -When a `.gitlab-ci.yml` file is present in a project, you can also use the CI Lint -tool to [simulate the creation of a full pipeline](lint.md#simulate-a-pipeline). -It does deeper verification of the configuration syntax. - -## Verify variables - -A key part of troubleshooting CI/CD is to verify which variables are present in a -pipeline, and what their values are. A lot of pipeline configuration is dependent -on variables, and verifying them is one of the fastest ways to find the source of -a problem. - -[Export the full list of variables](variables/index.md#list-all-variables) -available in each problematic job. Check if the variables you expect are present, -and check if their values are what you expect. - -## GitLab CI/CD documentation - -The [complete `.gitlab-ci.yml` reference](yaml/index.md) contains a full list of -every keyword you can use to configure your pipelines. - -You can also look at a large number of pipeline configuration [examples](examples/index.md) -and [templates](examples/index.md#cicd-templates). - -### Documentation for pipeline types - -Branch pipelines are the most basic type. -Other pipeline types have their own detailed usage guides that you should read -if you are using that type: - -- [Multi-project pipelines](pipelines/downstream_pipelines.md#multi-project-pipelines): Have your pipeline trigger - a pipeline in a different project. -- [Parent/child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines): Have your main pipeline trigger - and run separate pipelines in the same project. You can also - [dynamically generate the child pipeline's configuration](pipelines/downstream_pipelines.md#dynamic-child-pipelines) - at runtime. -- [Merge request pipelines](pipelines/merge_request_pipelines.md): Run a pipeline - in the context of a merge request. - - [Merged results pipelines](pipelines/merged_results_pipelines.md): - Merge request pipelines that run on the combined source and target branch - - [Merge trains](pipelines/merge_trains.md): - Multiple merged results pipelines that queue and run automatically before - changes are merged. - -### Troubleshooting Guides for CI/CD features - -Troubleshooting guides are available for some CI/CD features and related topics: - -- [Container Registry](../user/packages/container_registry/troubleshoot_container_registry.md) -- [GitLab Runner](https://docs.gitlab.com/runner/faq/) -- [Merge Trains](pipelines/merge_trains.md#troubleshooting) -- [Docker Build](docker/using_docker_build.md#troubleshooting) -- [Environments](environments/deployment_safety.md#ensure-only-one-deployment-job-runs-at-a-time) - -## Common CI/CD issues - -A lot of common pipeline issues can be fixed by analyzing the behavior of the `rules` -or `only/except` configuration. You shouldn't use these two configurations in the same -pipeline, as they behave differently. It's hard to predict how a pipeline runs with -this mixed behavior. - -If your `rules` or `only/except` configuration makes use of [predefined variables](variables/predefined_variables.md) -like `CI_PIPELINE_SOURCE`, `CI_MERGE_REQUEST_ID`, you should [verify them](#verify-variables) -as the first troubleshooting step. - -### Jobs or pipelines don't run when expected - -The `rules` or `only/except` keywords are what determine whether or not a job is -added to a pipeline. If a pipeline runs, but a job is not added to the pipeline, -it's usually due to `rules` or `only/except` configuration issues. - -If a pipeline does not seem to run at all, with no error message, it may also be -due to `rules` or `only/except` configuration, or the `workflow: rules` keyword. - -If you are converting from `only/except` to the `rules` keyword, you should check -the [`rules` configuration details](yaml/index.md#rules) carefully. The behavior -of `only/except` and `rules` is different and can cause unexpected behavior when migrating -between the two. - -The [common `if` clauses for `rules`](jobs/job_control.md#common-if-clauses-for-rules) -can be very helpful for examples of how to write rules that behave the way you expect. - -#### Two pipelines run at the same time - -Two pipelines can run when pushing a commit to a branch that has an open merge request -associated with it. Usually one pipeline is a merge request pipeline, and the other -is a branch pipeline. - -This situation is usually caused by the `rules` configuration, and there are several ways to -[prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines). - -#### A job is not in the pipeline - -GitLab determines if a job is added to a pipeline based on the [`only/except`](yaml/index.md#only--except) -or [`rules`](yaml/index.md#rules) defined for the job. If it didn't run, it's probably -not evaluating as you expect. - -#### No pipeline or the wrong type of pipeline runs - -Before a pipeline can run, GitLab evaluates all the jobs in the configuration and tries -to add them to all available pipeline types. A pipeline does not run if no jobs are added -to it at the end of the evaluation. - -If a pipeline did not run, it's likely that all the jobs had `rules` or `only/except` that -blocked them from being added to the pipeline. - -If the wrong pipeline type ran, then the `rules` or `only/except` configuration should -be checked to make sure the jobs are added to the correct pipeline type. For -example, if a merge request pipeline did not run, the jobs may have been added to -a branch pipeline instead. - -It's also possible that your [`workflow: rules`](yaml/index.md#workflow) configuration -blocked the pipeline, or allowed the wrong pipeline type. - -### Pipeline with many jobs fails to start - -A Pipeline that has more jobs than the instance's defined [CI/CD limits](../administration/settings/continuous_integration.md#set-cicd-limits) -fails to start. - -To reduce the number of jobs in your pipeline, you can split your `.gitlab-ci.yml` -configuration using [parent-child pipelines](../ci/pipelines/pipeline_architectures.md#parent-child-pipelines). - -### A job runs unexpectedly - -A common reason a job is added to a pipeline unexpectedly is because the `changes` -keyword always evaluates to true in certain cases. For example, `changes` is always -true in certain pipeline types, including scheduled pipelines and pipelines for tags. - -The `changes` keyword is used in combination with [`only/except`](yaml/index.md#onlychanges--exceptchanges) -or [`rules`](yaml/index.md#ruleschanges)). It's recommended to use `changes` with -`rules` or `only/except` configuration that ensures the job is only added to branch -pipelines or merge request pipelines. - -### "fatal: reference is not a tree" error - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17043) in GitLab 12.4. - -Previously, you'd have encountered unexpected pipeline failures when you force-pushed -a branch to its remote repository. To illustrate the problem, suppose you've had the current workflow: - -1. A user creates a feature branch named `example` and pushes it to a remote repository. -1. A new pipeline starts running on the `example` branch. -1. A user rebases the `example` branch on the latest default branch and force-pushes it to its remote repository. -1. A new pipeline starts running on the `example` branch again, however, - the previous pipeline (2) fails because of `fatal: reference is not a tree:` error. - -This occurs because the previous pipeline cannot find a checkout-SHA (which is associated with the pipeline record) -from the `example` branch that the commit history has already been overwritten by the force-push. -Similarly, [Merged results pipelines](pipelines/merged_results_pipelines.md) -might have failed intermittently due to [the same reason](pipelines/merged_results_pipelines.md#pipelines-fail-intermittently-with-a-fatal-reference-is-not-a-tree-error). - -As of GitLab 12.4, we've improved this behavior by persisting pipeline refs exclusively. -To illustrate its life cycle: - -1. A pipeline is created on a feature branch named `example`. -1. A persistent pipeline ref is created at `refs/pipelines/<pipeline-id>`, - which retains the checkout-SHA of the associated pipeline record. - This persistent ref stays intact during the pipeline execution, - even if the commit history of the `example` branch has been overwritten by force-push. -1. The runner fetches the persistent pipeline ref and gets source code from the checkout-SHA. -1. When the pipeline finishes, its persistent ref is cleaned up in a background process. - -### `get_sources` job section fails because of an HTTP/2 problem - -Sometimes, jobs fail with the following cURL error: - -```plaintext -++ git -c 'http.userAgent=gitlab-runner <version>' fetch origin +refs/pipelines/<id>:refs/pipelines/<id> ... -error: RPC failed; curl 16 HTTP/2 send again with decreased length -fatal: ... -``` - -You can work around this problem by configuring Git and `libcurl` to -[use HTTP/1.1](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpversion). -The configuration can be added to: - -- A job's [`pre_get_sources_script`](yaml/index.md#hookspre_get_sources_script): - - ```yaml - job_name: - hooks: - pre_get_sources_script: - - git config --global http.version "HTTP/1.1" - ``` - -- The [runner's `config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html) - with [Git configuration environment variables](https://git-scm.com/docs/git-config#ENVIRONMENT): - - ```toml - [[runners]] - ... - environment = [ - "GIT_CONFIG_COUNT=1", - "GIT_CONFIG_KEY_0=http.version", - "GIT_CONFIG_VALUE_0=HTTP/1.1" - ] - ``` - -### Merge request pipeline messages - -The merge request pipeline widget shows information about the pipeline status in -a merge request. It's displayed above the [ability to merge status widget](#merge-request-status-messages). - -#### "Checking ability to merge automatically" message - -There is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/229352) -where a merge request can be stuck with the `Checking ability to merge automatically` -message. - -If your merge request has this message and it does not disappear after a few minutes, -you can try one of these workarounds: - -- Refresh the merge request page. -- Close & Re-open the merge request. -- Rebase the merge request with the `/rebase` [quick action](../user/project/quick_actions.md). -- If you have already confirmed the merge request is ready to be merged, you can merge - it with the `/merge` quick action. - -#### "Checking pipeline status" message - -This message is shown when the merge request has no pipeline associated with the -latest commit yet. This might be because: - -- GitLab hasn't finished creating the pipeline yet. -- You are using an external CI service and GitLab hasn't heard back from the service yet. -- You are not using CI/CD pipelines in your project. -- You are using CI/CD pipelines in your project, but your configuration prevented a pipeline from running on the source branch for your merge request. -- The latest pipeline was deleted (this is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214323)). -- The source branch of the merge request is on a private fork. - -After the pipeline is created, the message updates with the pipeline status. - -### Merge request status messages - -The merge request status widget shows: - -- If the merge request is ready to merge. If the merge request can't be merged, the reason is displayed. -- **Merge**, if the pipeline is complete, or **Set to auto-merge** if the pipeline is still running. - -#### "A CI/CD pipeline must run and be successful before merge" message - -This message is shown if the [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#require-a-successful-pipeline-for-merge) -setting is enabled in the project and a pipeline has not yet run successfully. -This also applies if the pipeline has not been created yet, or if you are waiting -for an external CI service. If you don't use pipelines for your project, then you -should disable **Pipelines must succeed** so you can accept merge requests. - -#### "Merge blocked: pipeline must succeed. Push a new commit that fixes the failure" message - -This message is shown if the [merge request pipeline](pipelines/merge_request_pipelines.md), -[merged results pipeline](pipelines/merged_results_pipelines.md), -or [merge train pipeline](pipelines/merge_trains.md) -has failed or been canceled. -This does not happen when a basic branch pipeline fails. - -If a merge request pipeline or merged result pipeline was canceled or failed, you can: - -- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request. -- [Retry only the jobs that failed](pipelines/index.md#view-pipelines). If you re-run the entire pipeline, this is not necessary. -- Push a new commit to fix the failure. - -If the merge train pipeline has failed, you can: - -- Check the failure and determine if you can use the [`/merge` quick action](../user/project/quick_actions.md) to immediately add the merge request to the train again. -- Re-run the entire pipeline by selecting **Run pipeline** in the pipeline tab in the merge request, then add the merge request to the train again. -- Push a commit to fix the failure, then add the merge request to the train again. - -If the merge train pipeline was canceled before the merge request was merged, without a failure, you can: - -- Add it to the train again. - -### Merge request rules widget shows a scan result policy is invalid or duplicated **(ULTIMATE SELF)** - -On GitLab self-managed from 15.0 to 16.4, the most likely cause is that the project was exported from a -group and imported into another, and had scan result policy rules. These rules are stored in a -separate project to the one that was exported. As a result, the project contains policy rules that -reference entities that don't exist in the imported project's group. The result is policy rules that -are invalid, duplicated, or both. - -To remove all invalid scan result policy rules from a GitLab instance, an administrator can run -the following script in the [Rails console](../administration/operations/rails_console.md). - -```ruby -Project.joins(:approval_rules).where(approval_rules: { report_type: %i[scan_finding license_scanning] }).where.not(approval_rules: { security_orchestration_policy_configuration_id: nil }).find_in_batches.flat_map do |batch| - batch.map do |project| - # Get projects and their configuration_ids for applicable project rules - [project, project.approval_rules.where(report_type: %i[scan_finding license_scanning]).pluck(:security_orchestration_policy_configuration_id).uniq] - end.uniq.map do |project, configuration_ids| # We take only unique combinations of project + configuration_ids - # If we find more configurations than what is available for the project, we take records with the extra configurations - [project, configuration_ids - project.all_security_orchestration_policy_configurations.pluck(:id)] - end.select { |_project, configuration_ids| configuration_ids.any? } -end.each do |project, configuration_ids| - # For each found pair project + ghost configuration, we remove these rules for a given project - Security::OrchestrationPolicyConfiguration.where(id: configuration_ids).each do |configuration| - configuration.delete_scan_finding_rules_for_project(project.id) - end - # Ensure we sync any potential rules from new group's policy - Security::ScanResultPolicies::SyncProjectWorker.perform_async(project.id) -end -``` - -### Project `group/project` not found or access denied - -This message is shown if configuration is added with [`include`](yaml/index.md#include) and one of the following: - -- The configuration refers to a project that can't be found. -- The user that is running the pipeline is unable to access any included projects. - -To resolve this, check that: - -- The path of the project is in the format `my-group/my-project` and does not include - any folders in the repository. -- The user running the pipeline is a [member of the projects](../user/project/members/index.md#add-users-to-a-project) - that contain the included files. Users must also have the [permission](../user/permissions.md#job-permissions) - to run CI/CD jobs in the same projects. - -### "The parsed YAML is too big" message - -This message displays when the YAML configuration is too large or nested too deeply. -YAML files with a large number of includes, and thousands of lines overall, are -more likely to hit this memory limit. For example, a YAML file that is 200kb is -likely to hit the default memory limit. - -To reduce the configuration size, you can: - -- Check the length of the expanded CI/CD configuration in the pipeline editor's - [Full configuration](pipeline_editor/index.md#view-full-configuration) tab. Look for - duplicated configuration that can be removed or simplified. -- Move long or repeated `script` sections into standalone scripts in the project. -- Use [parent and child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines) to move some - work to jobs in an independent child pipeline. - -On a self-managed instance, you can [increase the size limits](../administration/instance_limits.md#maximum-size-and-depth-of-cicd-configuration-yaml-files). - -### Error 500 when editing the `.gitlab-ci.yml` file - -A [loop of included configuration files](pipeline_editor/index.md#configuration-validation-currently-not-available-message) -can cause a `500` error when editing the `.gitlab-ci.yml` file with the [web editor](../user/project/repository/web_editor.md). - -### A CI/CD job does not use newer configuration when run again - -The configuration for a pipeline is only fetched when the pipeline is created. -When you rerun a job, uses the same configuration each time. If you update configuration files, -including separate files added with [`include`](yaml/index.md#include), you must -start a new pipeline to use the new configuration. - -### Unable to pull image from another project - -> **Allow access to this project with a CI_JOB_TOKEN** setting [renamed to **Limit access _to_ this project**](https://gitlab.com/gitlab-org/gitlab/-/issues/411406) in GitLab 16.3. - -When a runner tries to pull an image from a private project, the job could fail with the following error: - -```shell -WARNING: Failed to pull image with policy "always": Error response from daemon: pull access denied for registry.example.com/path/to/project, repository does not exist or may require 'docker login': denied: requested access to the resource is denied -``` - -This error can happen if the following are both true: - -- The **Limit access _to_ this project** option is enabled in the private project - hosting the image. -- The job attempting to fetch the image is running for a project that is not listed in - the private project's allowlist. - -The recommended solution is to [add your project to the private project's job token scope allowlist](jobs/ci_job_token.md#add-a-project-to-the-job-token-scope-allowlist). - -## Pipeline warnings - -Pipeline configuration warnings are shown when you: - -- [Validate configuration with the CI Lint tool](yaml/index.md). -- [Manually run a pipeline](pipelines/index.md#run-a-pipeline-manually). - -### "Job may allow multiple pipelines to run for a single action" warning - -When you use [`rules`](yaml/index.md#rules) with a `when` clause without an `if` -clause, multiple pipelines may run. Usually this occurs when you push a commit to -a branch that has an open merge request associated with it. - -To [prevent duplicate pipelines](jobs/job_control.md#avoid-duplicate-pipelines), use -[`workflow: rules`](yaml/index.md#workflow) or rewrite your rules to control -which pipelines can run. - -### Console workaround if job using `resource_group` gets stuck **(FREE SELF)** - -```ruby -# find resource group by name -resource_group = Project.find_by_full_path('...').resource_groups.find_by(key: 'the-group-name') -busy_resources = resource_group.resources.where('build_id IS NOT NULL') - -# identify which builds are occupying the resource -# (I think it should be 1 as of today) -busy_resources.pluck(:build_id) - -# it's good to check why this build is holding the resource. -# Is it stuck? Has it been forcefully dropped by the system? -# free up busy resources -busy_resources.update_all(build_id: nil) -``` - -### Job log slow to update - -When you visit the job log page for a running job, there could be a delay of up to -60 seconds before the log updates. The default refresh time is 60 seconds, but after -the log is viewed in the UI, the following log updates should occur every 3 seconds. - -## Disaster recovery - -You can disable some important but computationally expensive parts of the application -to relieve stress on the database during ongoing downtime. - -### Disable fair scheduling on shared runners - -When clearing a large backlog of jobs, you can temporarily enable the `ci_queueing_disaster_recovery_disable_fair_scheduling` -[feature flag](../administration/feature_flags.md). This flag disables fair scheduling -on shared runners, which reduces system resource usage on the `jobs/request` endpoint. - -When enabled, jobs are processed in the order they were put in the system, instead of -balanced across many projects. - -### Disable compute quota enforcement - -To disable the enforcement of [compute quotas](pipelines/cicd_minutes.md) on shared runners, you can temporarily -enable the `ci_queueing_disaster_recovery_disable_quota` [feature flag](../administration/feature_flags.md). -This flag reduces system resource usage on the `jobs/request` endpoint. - -When enabled, jobs created in the last hour can run in projects which are out of quota. -Earlier jobs are already canceled by a periodic background worker (`StuckCiJobsWorker`). - -## CI/CD troubleshooting Rails console commands - -The following commands are run in the [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session). - -WARNING: -Any command that changes data directly could be damaging if not run correctly, or under the right conditions. -We highly recommend running them in a test environment with a backup of the instance ready to be restored, just in case. - -### Cancel stuck pending pipelines - -```ruby -project = Project.find_by_full_path('<project_path>') -Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count -Ci::Pipeline.where(project_id: project.id).where(status: 'pending').each {|p| p.cancel if p.stuck?} -Ci::Pipeline.where(project_id: project.id).where(status: 'pending').count -``` - -### Try merge request integration - -```ruby -project = Project.find_by_full_path('<project_path>') -mr = project.merge_requests.find_by(iid: <merge_request_iid>) -mr.project.try(:ci_integration) -``` - -### Validate the `.gitlab-ci.yml` file - -```ruby -project = Project.find_by_full_path('<project_path>') -content = p.ci_config_for(project.repository.root_ref_sha) -Gitlab::Ci::Lint.new(project: project, current_user: User.first).validate(content) -``` - -### Disable AutoDevOps on Existing Projects - -```ruby -Project.all.each do |p| - p.auto_devops_attributes={"enabled"=>"0"} - p.save -end -``` - -### Obtain runners registration token - -```ruby -Gitlab::CurrentSettings.current_application_settings.runners_registration_token -``` - -### Seed runners registration token - -```ruby -appSetting = Gitlab::CurrentSettings.current_application_settings -appSetting.set_runners_registration_token('<new-runners-registration-token>') -appSetting.save! -``` - -### Run pipeline schedules manually - -You can run pipeline schedules manually through the Rails console to reveal any errors that are usually not visible. - -```ruby -# schedule_id can be obtained from Edit Pipeline Schedule page -schedule = Ci::PipelineSchedule.find_by(id: <schedule_id>) - -# Select the user that you want to run the schedule for -user = User.find_by_username('<username>') - -# Run the schedule -ps = Ci::CreatePipelineService.new(schedule.project, user, ref: schedule.ref).execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule) -``` - -## How to get help - -If you are unable to resolve pipeline issues, you can get help from: - -- The [GitLab community forum](https://forum.gitlab.com/) -- GitLab [Support](https://about.gitlab.com/support/) +<!-- This redirect file can be deleted after <2024-02-01>. --> +<!-- Redirects that point to other docs in the same project expire in three months. --> +<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index ef573f6f3df..ed1bc190a4d 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -2026,7 +2026,7 @@ Use `hooks:pre_get_sources_script` to specify a list of commands to execute on t before cloning the Git repository and any submodules. You can use it for example to: -- Adjust the [Git configuration](../troubleshooting.md#get_sources-job-section-fails-because-of-an-http2-problem). +- Adjust the [Git configuration](../jobs/index.md#get_sources-job-section-fails-because-of-an-http2-problem). - Export [tracing variables](../../topics/git/useful_git_commands.md). **Possible inputs**: An array including: diff --git a/doc/development/index.md b/doc/development/index.md index 71ab54c8a73..abc19645ecb 100644 --- a/doc/development/index.md +++ b/doc/development/index.md @@ -10,7 +10,7 @@ description: "Development Guidelines: learn how to contribute to GitLab." Learn how to contribute to the development of the GitLab product. -This content is intended for GitLab team members as well as members of the wider community. +This content is intended for both GitLab team members and members of the wider community. - [Contribute to GitLab development](contributing/index.md) - [Contribute to GitLab Runner development](https://docs.gitlab.com/runner/development/) diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md index de081a7f620..685f1dbd99b 100644 --- a/doc/user/ai_features.md +++ b/doc/user/ai_features.md @@ -12,18 +12,18 @@ GitLab is creating AI-assisted features across our DevSecOps platform. These fea | Feature | Purpose | Large Language Model | Current availability | Maturity | |-|-|-|-|-| | [Suggested Reviewers](project/merge_requests/reviews/index.md#gitlab-duo-suggested-reviewers) | Assists in creating faster and higher-quality reviews by automatically suggesting reviewers for your merge request. | GitLab creates a machine learning model for each project, which is used to generate reviewers <br><br> [View the issue](https://gitlab.com/gitlab-org/modelops/applied-ml/applied-ml-updates/-/issues/10) | SaaS only <br><br> Ultimate tier | [Generally Available (GA)](../policy/experiment-beta-support.md#generally-available-ga) | -| [Code Suggestions](project/repository/code_suggestions/index.md) | Helps you write code more efficiently by viewing code suggestions as you type. | For Code Completion: Vertext AI Codey [`code-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-completion) and Anthropic [`Claude-instant-1.2`](https://docs.anthropic.com/claude/reference/selecting-a-model) <br><br> For Code Generation: Vertext AI Codey [`code-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-generation) and Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model)| [SaaS: All tiers](project/repository/code_suggestions/saas.md) <br><br> [Self-managed: Premium and Ultimate with Cloud Licensing](project/repository/code_suggestions/self_managed.md) | [Beta](../policy/experiment-beta-support.md#beta) | -| [Vulnerability summary](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | Helps you remediate vulnerabilities more efficiently, boost your skills, and write more secure code. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) <br><br> Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) if degraded performance | SaaS only <br><br> Ultimate tier | [Beta](../policy/experiment-beta-support.md#beta) | -| [Code explanation](#explain-code-in-the-web-ui-with-code-explanation) | Helps you understand code by explaining it in English language. | Vertext AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [GitLab Duo Chat](gitlab_duo_chat.md) | Process and generate text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) <br><br> Vertext AI Codey [`textembedding-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Code Suggestions](project/repository/code_suggestions/index.md) | Helps you write code more efficiently by viewing code suggestions as you type. | For Code Completion: Vertex AI Codey [`code-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-completion) and Anthropic [`Claude-instant-1.2`](https://docs.anthropic.com/claude/reference/selecting-a-model) <br><br> For Code Generation: Vertex AI Codey [`code-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-generation) and Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model)| [SaaS: All tiers](project/repository/code_suggestions/saas.md) <br><br> [Self-managed: Premium and Ultimate with Cloud Licensing](project/repository/code_suggestions/self_managed.md) | [Beta](../policy/experiment-beta-support.md#beta) | +| [Vulnerability summary](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | Helps you remediate vulnerabilities more efficiently, boost your skills, and write more secure code. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) <br><br> Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) if degraded performance | SaaS only <br><br> Ultimate tier | [Beta](../policy/experiment-beta-support.md#beta) | +| [Code explanation](#explain-code-in-the-web-ui-with-code-explanation) | Helps you understand code by explaining it in English language. | Vertex AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [GitLab Duo Chat](gitlab_duo_chat.md) | Process and generate text and code in a conversational manner. Helps you quickly identify useful information in large volumes of text in issues, epics, code, and GitLab documentation. | Anthropic [`Claude-2`](https://docs.anthropic.com/claude/reference/selecting-a-model) <br><br> Vertex AI Codey [`textembedding-gecko`](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | | [Value stream forecasting](#forecast-deployment-frequency-with-value-stream-forecasting) | Assists you with predicting productivity metrics and identifying anomalies across your software development lifecycle. | Statistical forecasting | SaaS only <br> Self-managed <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Discussion summary](#summarize-issue-discussions-with-discussion-summary) | Assists with quickly getting everyone up to speed on lengthy conversations to help ensure you are all on the same page. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Merge request summary](project/merge_requests/ai_in_merge_requests.md#summarize-merge-request-changes) | Efficiently communicate the impact of your merge request changes. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Code review summary](project/merge_requests/ai_in_merge_requests.md#summarize-my-merge-request-review) | Helps ease merge request handoff between authors and reviewers and help reviewers efficiently understand suggestions. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Merge request template population](project/merge_requests/ai_in_merge_requests.md#fill-in-merge-request-templates) | Generate a description for the merge request based on the contents of the template. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Test generation](project/merge_requests/ai_in_merge_requests.md#generate-suggested-tests-in-merge-requests) | Automates repetitive tasks and helps catch bugs early. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Git suggestions](https://gitlab.com/gitlab-org/gitlab/-/issues/409636) | Helps you discover or recall Git commands when and where you need them. | Vertext AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | -| [Root cause analysis](#root-cause-analysis) | Assists you in determining the root cause for a pipeline failure and failed CI/CD build. | Vertext AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Discussion summary](#summarize-issue-discussions-with-discussion-summary) | Assists with quickly getting everyone up to speed on lengthy conversations to help ensure you are all on the same page. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Merge request summary](project/merge_requests/ai_in_merge_requests.md#summarize-merge-request-changes) | Efficiently communicate the impact of your merge request changes. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Code review summary](project/merge_requests/ai_in_merge_requests.md#summarize-my-merge-request-review) | Helps ease merge request handoff between authors and reviewers and help reviewers efficiently understand suggestions. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Merge request template population](project/merge_requests/ai_in_merge_requests.md#fill-in-merge-request-templates) | Generate a description for the merge request based on the contents of the template. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Test generation](project/merge_requests/ai_in_merge_requests.md#generate-suggested-tests-in-merge-requests) | Automates repetitive tasks and helps catch bugs early. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Git suggestions](https://gitlab.com/gitlab-org/gitlab/-/issues/409636) | Helps you discover or recall Git commands when and where you need them. | Vertex AI Codey [`codechat-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/code-chat) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | +| [Root cause analysis](#root-cause-analysis) | Assists you in determining the root cause for a pipeline failure and failed CI/CD build. | Vertex AI Codey [`text-bison`](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | | [Issue description generation](#summarize-an-issue-with-issue-description-generation) | Generate issue descriptions. | OpenAI's [`GPT-3`](https://platform.openai.com/docs/models/gpt-3) | SaaS only <br><br> Ultimate tier | [Experiment](../policy/experiment-beta-support.md#experiment) | ## Enable AI/ML features diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md index 7582dd8bca3..d70463f4056 100644 --- a/doc/user/application_security/policies/scan-result-policies.md +++ b/doc/user/application_security/policies/scan-result-policies.md @@ -322,3 +322,35 @@ We have identified in [epic 11020](https://gitlab.com/groups/gitlab-org/-/epics/ - When using `newly_detected`, some findings may require approval when they are not introduced by the merge request (such as a new CVE on a related dependency). We currently use `main tip` of the target branch for comparison. In the future, we plan to use `merge base` for `newly_detected` policies (see [issue 428518](https://gitlab.com/gitlab-org/gitlab/-/issues/428518)). - Findings or errors that cause approval to be required on a scan result policy may not be evident in the Security MR Widget. By using `merge base` in [issue 428518](https://gitlab.com/gitlab-org/gitlab/-/issues/428518) some cases will be addressed. We will additionally be [displaying more granular details](https://gitlab.com/groups/gitlab-org/-/epics/11185) about what caused security policy violations. - Security policy violations are distinct compared to findings displayed in the MR widgets. Some violations may not be present in the MR widget. We are working to harmonize our features in [epic 11020](https://gitlab.com/groups/gitlab-org/-/epics/11020) and to display policy violations explicitly in merge requests in [epic 11185](https://gitlab.com/groups/gitlab-org/-/epics/11185). + +## Troubleshooting + +### Merge request rules widget shows a scan result policy is invalid or duplicated **(ULTIMATE SELF)** + +On GitLab self-managed from 15.0 to 16.4, the most likely cause is that the project was exported from a +group and imported into another, and had scan result policy rules. These rules are stored in a +separate project to the one that was exported. As a result, the project contains policy rules that +reference entities that don't exist in the imported project's group. The result is policy rules that +are invalid, duplicated, or both. + +To remove all invalid scan result policy rules from a GitLab instance, an administrator can run +the following script in the [Rails console](../../../administration/operations/rails_console.md). + +```ruby +Project.joins(:approval_rules).where(approval_rules: { report_type: %i[scan_finding license_scanning] }).where.not(approval_rules: { security_orchestration_policy_configuration_id: nil }).find_in_batches.flat_map do |batch| + batch.map do |project| + # Get projects and their configuration_ids for applicable project rules + [project, project.approval_rules.where(report_type: %i[scan_finding license_scanning]).pluck(:security_orchestration_policy_configuration_id).uniq] + end.uniq.map do |project, configuration_ids| # We take only unique combinations of project + configuration_ids + # If we find more configurations than what is available for the project, we take records with the extra configurations + [project, configuration_ids - project.all_security_orchestration_policy_configurations.pluck(:id)] + end.select { |_project, configuration_ids| configuration_ids.any? } +end.each do |project, configuration_ids| + # For each found pair project + ghost configuration, we remove these rules for a given project + Security::OrchestrationPolicyConfiguration.where(id: configuration_ids).each do |configuration| + configuration.delete_scan_finding_rules_for_project(project.id) + end + # Ensure we sync any potential rules from new group's policy + Security::ScanResultPolicies::SyncProjectWorker.perform_async(project.id) +end +``` diff --git a/doc/user/product_analytics/instrumentation/browser_sdk.md b/doc/user/product_analytics/instrumentation/browser_sdk.md index 912e157f67c..f2beafab8e0 100644 --- a/doc/user/product_analytics/instrumentation/browser_sdk.md +++ b/doc/user/product_analytics/instrumentation/browser_sdk.md @@ -161,6 +161,15 @@ glClient.page(eventAttributes); | :---------------- | :-------------------------- | :---------------------------------------------------------------- | | `eventAttributes` | `Object`/`Null`/`undefined` | The event attributes that need to be added to the pageview event. | +The `eventAttributes` object supports the following optional properties: + +| Property | Type | Description | +| :--------------- | :-------------------------- | :---------------------------------------------------------------------------- | +| `title` | `String` | Override the default page title. | +| `contextCallback` | `Function` | A callback that fires on the page view. | +| `context` | `Object` | Add context (additional information) on the page view. | +| `timestamp` | `timestamp` | Set the true timestamp or overwrite the device-sent timestamp on an event. | + ### `track` Used to trigger a custom event. diff --git a/lib/container_registry/gitlab_api_client.rb b/lib/container_registry/gitlab_api_client.rb index bd833ec00af..dd912caaf99 100644 --- a/lib/container_registry/gitlab_api_client.rb +++ b/lib/container_registry/gitlab_api_client.rb @@ -178,7 +178,7 @@ module ContainerRegistry req.params['n'] = limited_page_size req.params['last'] = last if last req.params['before'] = before if before - req.params['name'] = name if name + req.params['name'] = name if name.present? req.params['sort'] = sort if sort end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index bf44b74cf7b..70742e8bd38 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -5,16 +5,25 @@ module ContainerRegistry include Gitlab::Utils::StrongMemoize attr_reader :repository, :name, :updated_at - attr_writer :created_at + attr_writer :created_at, :manifest_digest, :revision, :total_size delegate :registry, :client, to: :repository - delegate :revision, :short_revision, to: :config_blob, allow_nil: true def initialize(repository, name) @repository = repository @name = name end + def revision + @revision || config_blob&.revision + end + + def short_revision + return unless revision + + revision[0..8] + end + def valid? manifest.present? end @@ -53,7 +62,7 @@ module ContainerRegistry def digest strong_memoize(:digest) do - client.repository_tag_digest(repository.path, name) + @manifest_digest || client.repository_tag_digest(repository.path, name) end end @@ -126,6 +135,8 @@ module ContainerRegistry # rubocop: disable CodeReuse/ActiveRecord def total_size + return @total_size if @total_size + return unless layers layers.sum(&:size) if v2? diff --git a/lib/gitlab/discussions_diff/file_collection.rb b/lib/gitlab/discussions_diff/file_collection.rb index 60b3a1738f1..3d1f7ab86b3 100644 --- a/lib/gitlab/discussions_diff/file_collection.rb +++ b/lib/gitlab/discussions_diff/file_collection.rb @@ -25,8 +25,9 @@ module Gitlab # # - Highlight cache is written just for uncached diff files # - The cache content is not updated (there's no need to do so) - def load_highlight - ids = highlightable_collection_ids + # - Load only the related diff note ids + def load_highlight(diff_note_ids: nil) + ids = highlightable_collection_ids(diff_note_ids) return if ids.empty? cached_content = read_cache(ids) @@ -47,8 +48,13 @@ module Gitlab private - def highlightable_collection_ids - each.with_object([]) { |file, memo| memo << file.id unless file.resolved_at } + def highlightable_collection_ids(diff_note_ids) + each.with_object([]) do |file, memo| + # We ignore if file is resolved, or not part of the highlight requested notes + next if file.resolved_at || (diff_note_ids.present? && diff_note_ids.exclude?(file.diff_note_id)) + + memo << file.id + end end def read_cache(ids) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a54d6bf92b5..7e222c286b2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14557,6 +14557,9 @@ msgstr "" msgid "Credit card required to be on file in order to create a pipeline" msgstr "" +msgid "Credit card validation record saved" +msgstr "" + msgid "Credit card:" msgstr "" @@ -19198,6 +19201,9 @@ msgstr "" msgid "Error rendering Markdown preview" msgstr "" +msgid "Error saving credit card validation record" +msgstr "" + msgid "Error saving label update." msgstr "" diff --git a/qa/qa/mobile/page/main/menu.rb b/qa/qa/mobile/page/main/menu.rb index 9bd2fbccbbb..73d3b9f7982 100644 --- a/qa/qa/mobile/page/main/menu.rb +++ b/qa/qa/mobile/page/main/menu.rb @@ -22,10 +22,10 @@ module QA end def open_mobile_menu - if has_no_element?('user-avatar-content') + if has_no_element?(:user_avatar_content) Support::Retrier.retry_until do click_element(:mobile_navbar_button) - has_element?('user-avatar-content') + has_element?(:user_avatar_content) end end end diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb index 3f411c05587..c80df535aba 100644 --- a/qa/qa/page/component/access_tokens.rb +++ b/qa/qa/page/component/access_tokens.rb @@ -15,12 +15,12 @@ module QA end base.view 'app/assets/javascripts/access_tokens/components/expires_at_field.vue' do - element 'expiry-date-field' + element :expiry_date_field end base.view 'app/views/shared/access_tokens/_form.html.haml' do - element 'access-token-name-field' - element 'create-token-button' + element :access_token_name_field + element :create_token_button end base.view 'app/views/shared/tokens/_scopes_form.html.haml' do @@ -28,16 +28,16 @@ module QA end base.view 'app/assets/javascripts/access_tokens/components/new_access_token_app.vue' do - element 'access-token-section' - element 'created-access-token-field' + element :access_token_section + element :created_access_token_field end base.view 'app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue' do - element 'toggle-visibility-button' + element :toggle_visibility_button end base.view 'app/assets/javascripts/access_tokens/components/access_token_table_app.vue' do - element 'revoke-button' + element :revoke_button end base.view 'app/views/profiles/personal_access_tokens/index.html.haml' do @@ -62,7 +62,7 @@ module QA end def fill_token_name(name) - fill_element('access-token-name-field', name) + fill_element(:access_token_name_field, name) end def check_api @@ -70,12 +70,12 @@ module QA end def click_create_token_button - click_element('create-token-button') + click_element(:create_token_button) end def created_access_token - within_element('access-token-section') do - find_element('created-access-token-field').value + within_element(:access_token_section) do + find_element(:created_access_token_field).value end end @@ -87,7 +87,7 @@ module QA raise "Expiry date must be in YYYY-MM-DD format" end - fill_element('expiry-date-field', date) + fill_element(:expiry_date_field, date) end def has_token_row_for_name?(token_name) @@ -100,7 +100,7 @@ module QA def revoke_first_token_with_name(token_name) within first_token_row_for_name(token_name) do - click_element('revoke-button') + click_element(:revoke_button) end click_confirmation_ok_button diff --git a/qa/qa/page/component/confirm_modal.rb b/qa/qa/page/component/confirm_modal.rb index fbc3cfa9aed..26d06ecaa22 100644 --- a/qa/qa/page/component/confirm_modal.rb +++ b/qa/qa/page/component/confirm_modal.rb @@ -15,24 +15,24 @@ module QA end base.view 'app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue' do - element 'confirm-danger-modal-button' - element 'confirm-danger-field' + element :confirm_danger_modal_button + element :confirm_danger_field end end def fill_confirmation_text(text) - fill_element('confirm-danger-field', text) + fill_element(:confirm_danger_field, text) end def wait_for_confirm_button_enabled wait_until(reload: false) do - !find_element('confirm-danger-modal-button').disabled? + !find_element(:confirm_danger_modal_button).disabled? end end def confirm_transfer wait_for_confirm_button_enabled - click_element('confirm-danger-modal-button') + click_element(:confirm_danger_modal_button) end def click_confirmation_ok_button diff --git a/qa/qa/page/component/members/invite_members_modal.rb b/qa/qa/page/component/members/invite_members_modal.rb index 66f5cef87f4..b9d0b382ba1 100644 --- a/qa/qa/page/component/members/invite_members_modal.rb +++ b/qa/qa/page/component/members/invite_members_modal.rb @@ -12,37 +12,37 @@ module QA super base.view 'app/assets/javascripts/invite_members/components/invite_modal_base.vue' do - element 'invite-modal-submit' - element 'access-level-dropdown' + element :invite_button + element :access_level_dropdown element 'invite-modal' end base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do - element 'members-token-select-input' + element :members_token_select_input end base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do - element 'invite-a-group-button' + element :invite_a_group_button end - base.view 'app/assets/javascripts/invite_members/components/invite_members_trigger.vue' do - element 'invite-members-button' + base.view 'app/assets/javascripts/invite_members/constants.js' do + element :invite_members_button end end def open_invite_members_modal - click_element 'invite-members-button' + click_element :invite_members_button end def open_invite_group_modal - click_element 'invite-a-group-button' + click_element :invite_a_group_button end def add_member(username, access_level = 'Developer', refresh_page: true) open_invite_members_modal within_element('invite-modal') do - fill_element('members-token-select-input', username) + fill_element(:members_token_select_input, username) Support::WaitForRequests.wait_for_requests click_button(username, match: :prefer_exact) set_access_level(access_level) @@ -68,7 +68,7 @@ module QA end def send_invite(refresh = false) - click_element 'invite-modal-submit' + click_element :invite_button Support::WaitForRequests.wait_for_requests page.refresh if refresh end @@ -77,7 +77,7 @@ module QA def set_access_level(access_level) # Guest option is selected by default, skipping these steps if desired option is 'Guest' - select_element('access-level-dropdown', access_level) unless access_level == 'Guest' + select_element(:access_level_dropdown, access_level) unless access_level == 'Guest' end end end diff --git a/qa/qa/page/group/settings/group_deploy_tokens.rb b/qa/qa/page/group/settings/group_deploy_tokens.rb index c76c910e857..c1c3303113b 100644 --- a/qa/qa/page/group/settings/group_deploy_tokens.rb +++ b/qa/qa/page/group/settings/group_deploy_tokens.rb @@ -6,58 +6,58 @@ module QA module Settings class GroupDeployTokens < Page::Base view 'app/views/shared/deploy_tokens/_form.html.haml' do - element 'deploy-token-name-field' - element 'deploy-token-expires-at-field' - element 'deploy-token-read-repository-checkbox' - element 'deploy-token-read-package-registry-checkbox' - element 'deploy-token-read-registry-checkbox' - element 'deploy-token-write-package-registry-checkbox' - element 'create-deploy-token-button' + element :deploy_token_name_field + element :deploy_token_expires_at_field + element :deploy_token_read_repository_checkbox + element :deploy_token_read_package_registry_checkbox + element :deploy_token_read_registry_checkbox + element :deploy_token_write_package_registry_checkbox + element :create_deploy_token_button end view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do - element 'created-deploy-token-container' - element 'deploy-token-user-field' - element 'deploy-token-field' + element :created_deploy_token_container + element :deploy_token_user_field + element :deploy_token_field end def fill_token_name(name) - fill_element('deploy-token-name-field', name) + fill_element(:deploy_token_name_field, name) end def fill_token_expires_at(expires_at) - fill_element('deploy-token-expires-at-field', expires_at.to_s + "\n") + fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n") end def fill_scopes(read_repository: false, read_registry: false, read_package_registry: false, write_package_registry: false ) - check_element('deploy-token-read-repository-checkbox', true) if read_repository - check_element('deploy-token-read-package-registry-checkbox', true) if read_package_registry - check_element('deploytoken-read-registry-checkbox', true) if read_registry - check_element('deploy-token-write-package-registry-checkbox', true) if write_package_registry + check_element(:deploy_token_read_repository_checkbox, true) if read_repository + check_element(:deploy_token_read_package_registry_checkbox, true) if read_package_registry + check_element(:deploy_token_read_registry_checkbox, true) if read_registry + check_element(:deploy_token_write_package_registry_checkbox, true) if write_package_registry end def add_token - click_element('create-deploy-token-button') + click_element(:create_deploy_token_button) end def token_username within_new_project_deploy_token do - find_element('deploy-token-user-field').value + find_element(:deploy_token_user_field).value end end def token_password within_new_project_deploy_token do - find_element('deploy-token-field').value + find_element(:deploy_token_field).value end end private def within_new_project_deploy_token(&block) - has_element?('created-deploy-token-container', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) + has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) - within_element('created-deploy-token-container', &block) + within_element(:created_deploy_token_container, &block) end end end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index a1c496a1f57..1fd0b5b453c 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -7,55 +7,55 @@ module QA include Layout::Flash view 'app/views/devise/passwords/edit.html.haml' do - element 'password-field' - element 'password-confirmation-field' - element 'change-password-button' + element :password_field + element :password_confirmation_field + element :change_password_button end view 'app/views/devise/sessions/new.html.haml' do - element 'register-link' + element :register_link end view 'app/views/devise/sessions/_new_base.html.haml' do - element 'username-field' - element 'password-field' - element 'sign-in-button' + element :login_field + element :password_field + element :sign_in_button end view 'app/views/devise/sessions/_new_ldap.html.haml' do - element 'username-field' - element 'password-field' - element 'sign-in-button' + element :username_field + element :password_field + element :sign_in_button end view 'app/views/devise/shared/_tabs_ldap.html.haml' do - element 'ldap-tab' - element 'standard-tab' - element 'register-tab' + element :ldap_tab + element :standard_tab + element :register_tab end view 'app/views/devise/shared/_tab_single.html.haml' do - element 'sign-in-tab' + element :sign_in_tab end view 'app/helpers/auth_helper.rb' do - element 'saml-login-button' - element 'github-login-button' - element 'oidc-login-button' - element 'gitlab-oauth-login-button' - element 'facebook-login-button' + element :saml_login_button + element :github_login_button + element :oidc_login_button + element :gitlab_oauth_login_button + element :facebook_login_button end view 'app/views/layouts/devise.html.haml' do - element 'login-page', required: true + element :login_page, required: true end def can_sign_in? - has_element?('sign-in-button') + has_element?(:sign_in_button) end def on_login_page? - has_element?('login-page', wait: 0) + has_element?(:login_page, wait: 0) end def sign_in_using_credentials(user: nil, skip_page_validation: false) @@ -98,9 +98,9 @@ module QA switch_to_ldap_tab - fill_element 'username-field', user.ldap_username - fill_element 'password-field', user.ldap_password - click_element 'sign-in-button' + fill_element :username_field, user.ldap_username + fill_element :password_field, user.ldap_password + click_element :sign_in_button end Page::Main::Menu.perform(&:signed_in?) @@ -130,15 +130,15 @@ module QA end def has_sign_in_tab?(wait: Capybara.default_max_wait_time) - has_element?('sign-in-tab', wait: wait) + has_element?(:sign_in_tab, wait: wait) end def has_ldap_tab? - has_element?('ldap-tab') + has_element?(:ldap_tab) end def has_standard_tab? - has_element?('standard-tab') + has_element?(:standard_tab) end def sign_in_tab? @@ -162,45 +162,45 @@ module QA end def switch_to_sign_in_tab - click_element 'sign-in-tab' + click_element :sign_in_tab end def switch_to_register_page set_initial_password_if_present - click_element 'register-link' + click_element :register_link end def switch_to_ldap_tab - click_element 'ldap-tab' + click_element :ldap_tab end def switch_to_standard_tab - click_element 'standard-tab' + click_element :standard_tab end def sign_in_with_github set_initial_password_if_present - click_element 'github-login-button' + click_element :github_login_button end def sign_in_with_facebook set_initial_password_if_present - click_element 'facebook-login-button' + click_element :facebook_login_button end def sign_in_with_saml set_initial_password_if_present - click_element 'saml-login-button' + click_element :saml_login_button end def sign_in_with_gitlab_oidc set_initial_password_if_present - click_element 'oidc-login-button' + click_element :oidc_login_button end def sign_in_with_gitlab_oauth set_initial_password_if_present - click_element 'gitlab-oauth-login-button' + click_element :gitlab_oauth_login_button end def sign_out_and_sign_in_as(user:) @@ -233,7 +233,7 @@ module QA click_accept_all_cookies if Runtime::Env.running_on_dot_com? && has_accept_all_cookies_button? - click_element 'sign-in-button' + click_element :sign_in_button Support::WaitForRequests.wait_for_requests @@ -254,16 +254,16 @@ module QA end def fill_in_credential(user) - fill_element 'username-field', user.username - fill_element 'password-field', user.password + fill_element :login_field, user.username + fill_element :password_field, user.password end def set_initial_password_if_present return unless has_content?('Change your password') - fill_element 'password-field', Runtime::User.password - fill_element 'password-confirmation-field', Runtime::User.password - click_element 'change-password-button' + fill_element :password_field, Runtime::User.password + fill_element :password_confirmation_field, Runtime::User.password + click_element :change_password_button end end end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index af446291b3c..73d48c6fcbf 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -16,9 +16,9 @@ module QA view 'app/assets/javascripts/super_sidebar/components/user_menu.vue' do element 'user-dropdown', required: !Runtime::Env.phone_layout? - element 'user-avatar-content', required: !Runtime::Env.phone_layout? + element :user_avatar_content, required: !Runtime::Env.phone_layout? element :sign_out_link - element 'edit-profile-link' + element :edit_profile_link end view 'app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue' do @@ -118,7 +118,7 @@ module QA has_element?('user-profile-link', text: /#{user.username}/) end # we need to close user menu because plain user link check will leave it open - click_element 'user-avatar-content' if has_element?('user-profile-link', wait: 0) + click_element :user_avatar_content if has_element?('user-profile-link', wait: 0) end def not_signed_in? @@ -150,7 +150,7 @@ module QA def click_edit_profile_link retry_until(reload: false) do within_user_menu do - click_element('edit-profile-link') + click_element(:edit_profile_link) end has_text?('User Settings') @@ -164,11 +164,11 @@ module QA end def has_personal_area?(wait: Capybara.default_max_wait_time) - has_element?('user-avatar-content', wait: wait) + has_element?(:user_avatar_content, wait: wait) end def has_no_personal_area?(wait: Capybara.default_max_wait_time) - has_no_element?('user-avatar-content', wait: wait) + has_no_element?(:user_avatar_content, wait: wait) end def click_stop_impersonation_link @@ -189,7 +189,7 @@ module QA def within_user_menu(&block) within_element(:navbar) do - click_element 'user-avatar-content' unless has_element?('user-profile-link', wait: 1) + click_element :user_avatar_content unless has_element?('user-profile-link', wait: 1) within_element('user-dropdown', &block) end diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb index 2ac99df905a..2b1a9ab2b6a 100644 --- a/qa/qa/page/main/oauth.rb +++ b/qa/qa/page/main/oauth.rb @@ -5,7 +5,7 @@ module QA module Main class OAuth < Page::Base view 'app/views/doorkeeper/authorizations/new.html.haml' do - element 'authorization_button' + element :authorization_button end def needs_authorization? @@ -13,7 +13,7 @@ module QA end def authorize! - click_element 'authorization_button' + click_element :authorization_button end end end diff --git a/qa/qa/page/main/terms.rb b/qa/qa/page/main/terms.rb index 1443c5b56f3..24f6b03549b 100644 --- a/qa/qa/page/main/terms.rb +++ b/qa/qa/page/main/terms.rb @@ -5,17 +5,17 @@ module QA module Main class Terms < Page::Base view 'app/views/layouts/terms.html.haml' do - element 'user-avatar-content', required: true + element :user_avatar_content, required: true end view 'app/assets/javascripts/terms/components/app.vue' do - element 'terms-content', required: true + element :terms_content, required: true - element 'accept-terms-button' + element :accept_terms_button end def accept_terms - click_element 'accept-terms-button', Page::Main::Menu + click_element :accept_terms_button, Page::Main::Menu end end end diff --git a/qa/qa/page/main/two_factor_auth.rb b/qa/qa/page/main/two_factor_auth.rb index 186027900ca..003bd8dd1b1 100644 --- a/qa/qa/page/main/two_factor_auth.rb +++ b/qa/qa/page/main/two_factor_auth.rb @@ -5,16 +5,16 @@ module QA module Main class TwoFactorAuth < Page::Base view 'app/views/devise/sessions/two_factor.html.haml' do - element 'verify-code-button' - element 'two-fa-code-field' + element :verify_code_button + element :two_fa_code_field end def click_verify_code_button - click_element 'verify-code-button' + click_element :verify_code_button end def set_2fa_code(code) - fill_element('two-fa-code-field', code) + fill_element(:two_fa_code_field, code) end end end diff --git a/qa/qa/page/profile/accounts/show.rb b/qa/qa/page/profile/accounts/show.rb index b29c1d9da12..84a34d1da78 100644 --- a/qa/qa/page/profile/accounts/show.rb +++ b/qa/qa/page/profile/accounts/show.rb @@ -6,24 +6,24 @@ module QA module Accounts class Show < Page::Base view 'app/views/profiles/accounts/show.html.haml' do - element 'delete-account-button', required: true - element 'enable-2fa-button' + element :delete_account_button, required: true + element :enable_2fa_button end view 'app/assets/javascripts/profile/account/components/delete_account_modal.vue' do - element 'password-confirmation-field' - element 'confirm-delete-account-button' + element :password_confirmation_field + element :confirm_delete_account_button end def click_enable_2fa_button - click_element('enable-2fa-button') + click_element(:enable_2fa_button) end def delete_account(password) - click_element('delete-account-button') + click_element(:delete_account_button) - find_element('password-confirmation-field').set password - click_element('confirm-delete-account-button') + find_element(:password_confirmation_field).set password + click_element(:confirm_delete_account_button) end end end diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb index c9ec056261a..2990ba6a4ac 100644 --- a/qa/qa/page/profile/ssh_keys.rb +++ b/qa/qa/page/profile/ssh_keys.rb @@ -11,7 +11,7 @@ module QA end view 'app/assets/javascripts/access_tokens/components/expires_at_field.vue' do - element 'expiry-date-field' + element :expiry_date_field end view 'app/helpers/ssh_keys_helper.rb' do @@ -31,7 +31,7 @@ module QA # Expire in 2 days just in case the key is created just before midnight fill_expiry_date(Date.today + 2) # Close the datepicker - find_element('expiry-date-field').find('input').send_keys(:enter) + find_element(:expiry_date_field).find('input').send_keys(:enter) click_element(:add_key_button) end @@ -44,7 +44,7 @@ module QA raise "Expiry date must be in YYYY-MM-DD format" end - fill_element('expiry-date-field', date) + fill_element(:expiry_date_field, date) end def remove_key(title) diff --git a/qa/qa/page/profile/two_factor_auth.rb b/qa/qa/page/profile/two_factor_auth.rb index 980d4a74a8c..2add02b5c48 100644 --- a/qa/qa/page/profile/two_factor_auth.rb +++ b/qa/qa/page/profile/two_factor_auth.rb @@ -5,61 +5,61 @@ module QA module Profile class TwoFactorAuth < Page::Base view 'app/assets/javascripts/pages/profiles/two_factor_auths/index.js' do - element 'configure-it-later-button' + element :configure_it_later_button end view 'app/views/profiles/two_factor_auths/show.html.haml' do - element 'otp-secret-content' - element 'pin-code-field' - element 'current-password-field' - element 'register-2fa-app-button' + element :otp_secret_content + element :pin_code_field + element :current_password_field + element :register_2fa_app_button end view 'app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue' do - element 'proceed-button' - element 'copy-button' - element 'recovery-codes' - element 'code-content' + element :proceed_button + element :copy_button + element :codes_content + element :code_content end def click_configure_it_later_button # TO DO: Investigate why button does not appear sometimes: # https://gitlab.com/gitlab-org/gitlab/-/issues/382698 page.refresh - return unless has_element?('configure-it-later-button', wait: 60) + return unless has_element?(:configure_it_later_button, wait: 60) - click_element 'configure-it-later-button' + click_element :configure_it_later_button wait_until(max_duration: 10, message: "Waiting for create a group page") do has_text?("Welcome to GitLab") && has_text?("Create a group") end end def otp_secret_content - find_element('otp-secret-content').text.gsub('Key:', '').delete(' ') + find_element(:otp_secret_content).text.gsub('Key:', '').delete(' ') end def set_pin_code(pin_code) - fill_element('pin-code-field', pin_code) + fill_element(:pin_code_field, pin_code) end def set_current_password(password) - fill_element('current-password-field', password) + fill_element(:current_password_field, password) end def click_register_2fa_app_button - click_element 'register-2fa-app-button' + click_element :register_2fa_app_button end def recovery_codes - code_elements = within_element('recovery-codes') do - all_elements('code-content', minimum: 1) + code_elements = within_element(:codes_content) do + all_elements(:code_content, minimum: 1) end code_elements.map { |code_content| code_content.text } end def click_copy_and_proceed - click_element 'copy-button' - click_element 'proceed-button' + click_element :copy_button + click_element :proceed_button end end end diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb index a1ba270a42f..cf25f4a0568 100644 --- a/qa/qa/page/project/settings/deploy_tokens.rb +++ b/qa/qa/page/project/settings/deploy_tokens.rb @@ -6,74 +6,74 @@ module QA module Settings class DeployTokens < Page::Base view 'app/views/shared/deploy_tokens/_form.html.haml' do - element 'deploy-token-name-field' - element 'deploy-token-expires-at-field' - element 'deploy-token-read-repository-checkbox' - element 'deploy-token-read-package-registry-checkbox' - element 'deploy-token-write-package-registry-checkbox' - element 'deploy-token-read-registry-checkbox' - element 'deploy-token-write-registry-checkbox' - element 'create-deploy-token-button' + element :deploy_token_name_field + element :deploy_token_expires_at_field + element :deploy_token_read_repository_checkbox + element :deploy_token_read_package_registry_checkbox + element :deploy_token_write_package_registry_checkbox + element :deploy_token_read_registry_checkbox + element :deploy_token_write_registry_checkbox + element :create_deploy_token_button end view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do - element 'created-deploy-token-container' - element 'deploy-token-user-field' - element 'deploy-token-field' + element :created_deploy_token_container + element :deploy_token_user_field + element :deploy_token_field end def fill_token_name(name) - fill_element('deploy-token-name-field', name) + fill_element(:deploy_token_name_field, name) end def fill_token_expires_at(expires_at) - fill_element('deploy-token-expires-at-field', expires_at.to_s + "\n") + fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n") end def fill_scopes(scopes) if scopes.include? :read_repository - check_element('deploy-token-read-repository-checkbox', true) + check_element(:deploy_token_read_repository_checkbox, true) end if scopes.include? :read_package_registry - check_element('deploy-token-read-package-registry-checkbox', true) + check_element(:deploy_token_read_package_registry_checkbox, true) end if scopes.include? :write_package_registry - check_element('deploy-token-write-package-registry-checkbox', true) + check_element(:deploy_token_write_package_registry_checkbox, true) end if scopes.include? :read_registry - check_element('deploy-token-read-registry-checkbox', true) + check_element(:deploy_token_read_registry_checkbox, true) end if scopes.include? :write_registry - check_element('deploy-token-write-registry-checkbox', true) + check_element(:deploy_token_write_registry_checkbox, true) end end def add_token - click_element('create-deploy-token-button') + click_element(:create_deploy_token_button) end def token_username within_new_project_deploy_token do - find_element('deploy-token-user-field').value + find_element(:deploy_token_user_field).value end end def token_password within_new_project_deploy_token do - find_element('deploy-token-field').value + find_element(:deploy_token_field).value end end private def within_new_project_deploy_token - has_element?('created-deploy-token-container', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) + has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) - within_element('created-deploy-token-container') do + within_element(:created_deploy_token_container) do yield end end diff --git a/scripts/duo_chat/reporter.rb b/scripts/duo_chat/reporter.rb new file mode 100755 index 00000000000..f13481cf41b --- /dev/null +++ b/scripts/duo_chat/reporter.rb @@ -0,0 +1,231 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'gitlab' +require 'json' + +class Reporter + IDENTIFIABLE_NOTE_TAG = 'gitlab-org/ai-powered/ai-framework:duo-chat-qa-evaluation-' + + GRADE_TO_EMOJI_MAPPING = { + correct: ":white_check_mark:", + incorrect: ":x:", + unexpected: ":warning:" + }.freeze + + def run + merge_request_iid = ENV['CI_MERGE_REQUEST_IID'] + ci_project_id = ENV['CI_PROJECT_ID'] + + puts "Saving #{artifact_path}" + File.write(artifact_path, report_note) + + # Look for an existing note + report_notes = com_gitlab_client + .merge_request_notes(ci_project_id, merge_request_iid) + .auto_paginate + .select do |note| + note.body.include? note_identifier_tag + end + + note = report_notes.max_by { |note| Time.parse(note.created_at) } + + if note && note.type != 'DiscussionNote' + # The latest note has not led to a discussion. Update it. + com_gitlab_client.edit_merge_request_note(ci_project_id, merge_request_iid, note.id, report_note) + + puts "Updated comment." + else + # This is the first note or the latest note has been discussed on the MR. + # Don't update, create new note instead. + com_gitlab_client.create_merge_request_note(ci_project_id, merge_request_iid, report_note) + + puts "Posted comment." + end + end + + private + + def report_filename + "#{ENV['DUO_RSPEC']}.md" + end + + def artifact_path + File.join(ENV['CI_PROJECT_DIR'], report_filename) + end + + def note_identifier_tag + "#{IDENTIFIABLE_NOTE_TAG}#{ENV['DUO_RSPEC']}" + end + + def com_gitlab_client + @com_gitlab_client ||= Gitlab.client( + endpoint: "https://gitlab.com/api/v4", + private_token: ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] + ) + end + + def report_note + report = <<~MARKDOWN + <!-- #{note_identifier_tag} --> + + ## GitLab Duo Chat QA evaluation + + Report generated for "#{ENV['CI_JOB_NAME']}". This report is generated and refreshed automatically. Do not edit. + + LLMs have been asked to evaluate GitLab Duo Chat's answers. + + :white_check_mark: : LLM evaluated the answer as `CORRECT`. + + :x: : LLM evaluated the answer as `INCORRECT`. + + :warning: : LLM did not evaluate correctly or the evaluation request might have failed. + + ### Summary + + - The total number of evaluations: #{summary_numbers[:total]} + + - The number of evaluations in which all LLMs graded `CORRECT`: #{summary_numbers[:correct]} (#{summary_numbers[:correct_ratio]}%) + + - Note: if an evaluation request failed or its response was not parsable, it was ignored. For example, :white_check_mark: :warning: would count as `CORRECT`. + + - The number of evaluations in which all LLMs graded `INCORRECT`: #{summary_numbers[:incorrect]} (#{summary_numbers[:incorrect_ratio]}%) + + - Note: if an evaluation request failed or its response was not parsable, it was ignored. For example, :x: :warning: would count as `INCORRECT`. + + - The number of evaluations in which LLMs disagreed: #{summary_numbers[:disagreed]} (#{summary_numbers[:disagreed_ratio]}%) + + + ### Evaluations + + #{eval_content} + + + MARKDOWN + + if report.length > 1000000 + return <<~MARKDOWN + <!-- #{note_identifier_tag} --> + + ## GitLab Duo Chat QA evaluation + + Report generated for "#{ENV['CI_JOB_NAME']}". This report is generated and refreshed automatically. Do not edit. + + **:warning: the evaluation report is too long (> `1000000`) and cannot be posted as a note.** + + Please check out the artifact for the CI job "#{ENV['CI_JOB_NAME']}": + + https://gitlab.com/gitlab-org/gitlab/-/jobs/#{ENV['CI_JOB_ID']}/artifacts/file/#{report_filename} + + MARKDOWN + end + + report + end + + def report_data + @report_data ||= Dir[File.join(ENV['CI_PROJECT_DIR'], "tmp/duo_chat/qa*.json")] + .map { |file| JSON.parse(File.read(file)) } + end + + def eval_content + report_data + .sort_by { |a| a["question"] } + .map do |data| + <<~MARKDOWN + <details> + + <summary> + + #{correctness_indicator(data)} + + `"#{data['question']}"` + + (context: `#{data['resource']}`) + + </summary> + + #### Resource + + `#{data['resource']}` + + #### Answer + + #{data['answer']} + + #### LLM Evaluation + + #{evalutions(data)} + + + </details> + + MARKDOWN + end + .join + end + + def summary_numbers + @graded_evaluations ||= report_data.map { |data| data["evaluations"].map { |eval| parse_grade(eval) } } + + total = @graded_evaluations.size + correct = @graded_evaluations.count { |grades| !(grades.include? :incorrect) } + incorrect = @graded_evaluations.count { |grades| !(grades.include? :correct) } + disagreed = @graded_evaluations.count { |grades| (grades.include? :correct) && (grades.include? :incorrect) } + + { + total: total, + correct: correct, + correct_ratio: (correct.to_f / total * 100).round(1), + incorrect: incorrect, + incorrect_ratio: (incorrect.to_f / total * 100).round(1), + disagreed: disagreed, + disagreed_ratio: (disagreed.to_f / total * 100).round(1) + } + end + + def parse_grade(eval) + return :correct if eval["response"].match?(/Grade: CORRECT/i) + return :incorrect if eval["response"].match?(/Grade: INCORRECT/i) + + # If the LLM's evaluation includes neither CORRECT nor CORRECT, flag it. + :unexpected + end + + def correctness_indicator(data) + data["evaluations"].map { |eval| parse_grade(eval) }.map { |grade| GRADE_TO_EMOJI_MAPPING[grade] }.join(' ') + end + + def evalutions(data) + rows = data["evaluations"].map do |eval| + grade = parse_grade(eval) + + <<~MARKDOWN + <tr> + <td>#{eval['model']}</td> + <td> + #{GRADE_TO_EMOJI_MAPPING[grade]} + </td> + <td> + #{eval['response']} + </td + </tr> + + MARKDOWN + end + .join + + <<~MARKDOWN + <table> + <tr> + <td>Model</td> + <td>Grade</td> + <td>Details</td> + </tr> + #{rows} + </table> + MARKDOWN + end +end + +Reporter.new.run diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 92bbffdfde5..539c6d17e0e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -2305,68 +2305,139 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review end context 'highlight preloading' do - context 'with commit diff notes' do - let!(:commit_diff_note) do - create(:diff_note_on_commit, project: merge_request.project) + context 'when only_highlight_discussions_requested is false' do + before do + stub_feature_flags(only_highlight_discussions_requested: false) end - it 'preloads notes diffs highlights' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - note_diff_file = commit_diff_note.note_diff_file + context 'with commit diff notes' do + let!(:first_commit_diff_note) do + create(:diff_note_on_commit, project: merge_request.project) + end + + let!(:second_commit_diff_note) do + create(:diff_note_on_commit, project: merge_request.project) + end + + it 'preloads all of the notes diffs highlights' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + first_note_diff_file = first_commit_diff_note.note_diff_file + second_note_diff_file = second_commit_diff_note.note_diff_file + + expect(collection).to receive(:load_highlight).and_call_original + expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original + expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original + end - expect(collection).to receive(:load_highlight).and_call_original - expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, + per_page: 2 } end - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + it 'preloads all of the notes diffs highlights when per_page is 1' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + first_note_diff_file = first_commit_diff_note.note_diff_file + second_note_diff_file = second_commit_diff_note.note_diff_file + + expect(collection).to receive(:load_highlight).and_call_original + expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original + expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id) + end + + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, + per_page: 1 } + end end - end - context 'with diff notes' do - let!(:diff_note) do - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) + context 'with diff notes' do + let!(:diff_note) do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) + end + + it 'preloads notes diffs highlights' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + note_diff_file = diff_note.note_diff_file + + expect(collection).to receive(:load_highlight).and_call_original + expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original + end + + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + end end + end - it 'preloads notes diffs highlights' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - note_diff_file = diff_note.note_diff_file + context 'when only_highlight_discussions_requested is true' do + context 'with commit diff notes' do + let!(:first_commit_diff_note) do + create(:diff_note_on_commit, project: merge_request.project) + end - expect(collection).to receive(:load_highlight).and_call_original - expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original + let!(:second_commit_diff_note) do + create(:diff_note_on_commit, project: merge_request.project) end - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } - end + it 'preloads all of the notes diffs highlights' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + first_note_diff_file = first_commit_diff_note.note_diff_file + second_note_diff_file = second_commit_diff_note.note_diff_file + + expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id, second_commit_diff_note.id]).and_call_original + expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original + expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original + end + + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, + per_page: 2 } + end + + it 'preloads all of the notes diffs highlights when per_page is 1' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + first_note_diff_file = first_commit_diff_note.note_diff_file + second_note_diff_file = second_commit_diff_note.note_diff_file - it 'does not preload highlights when diff note is resolved' do - Notes::ResolveService.new(diff_note.project, user).execute(diff_note) + expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id]).and_call_original + expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original + expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id) + end - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - note_diff_file = diff_note.note_diff_file + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, + per_page: 1 } + end + end - expect(collection).to receive(:load_highlight).and_call_original - expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original + context 'with diff notes' do + let!(:diff_note) do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) end - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + it 'preloads notes diffs highlights' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + note_diff_file = diff_note.note_diff_file + + expect(collection).to receive(:load_highlight).with(diff_note_ids: [diff_note.id]).and_call_original + expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original + end + + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + end end end end - end - context do - it_behaves_like 'discussions provider' do - let!(:author) { create(:user) } - let!(:project) { create(:project) } + context do + it_behaves_like 'discussions provider' do + let!(:author) { create(:user) } + let_it_be_with_refind(:project) { create(:project) } - let!(:merge_request) { create(:merge_request, source_project: project) } + let!(:merge_request) { create(:merge_request, source_project: project) } - let!(:mr_note1) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) } - let!(:mr_note2) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) } + let!(:mr_note1) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) } + let!(:mr_note2) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) } - let(:requested_iid) { merge_request.iid } - let(:expected_discussion_count) { 2 } - let(:expected_discussion_ids) { [mr_note1.discussion_id, mr_note2.discussion_id] } + let(:requested_iid) { merge_request.iid } + let(:expected_discussion_count) { 2 } + let(:expected_discussion_ids) { [mr_note1.discussion_id, mr_note2.discussion_id] } + end end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 2ae9d87cc64..0962c251382 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -94,6 +94,8 @@ FactoryBot.define do visibility_level: evaluator.visibility_level } + project_namespace_hash[:id] = evaluator.project_namespace_id.presence + project.build_project_namespace(project_namespace_hash) project.build_project_feature(project_feature_hash) diff --git a/spec/features/projects/members/manage_groups_spec.rb b/spec/features/projects/members/manage_groups_spec.rb index 3a67879e33d..63ff1ba8455 100644 --- a/spec/features/projects/members/manage_groups_spec.rb +++ b/spec/features/projects/members/manage_groups_spec.rb @@ -228,6 +228,6 @@ RSpec.describe 'Project > Members > Manage groups', :js, feature_category: :grou end def invite_group_selector - 'button[data-testid="invite-a-group-button"]' + 'button[data-test-id="invite-group-button"]' end end diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap index 7785693ff2a..2bd2b17a12d 100644 --- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap +++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap @@ -11,7 +11,7 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi arialabel="" autocomplete="" container="" - data-testid="expiry-date-field" + data-qa-selector="expiry_date_field" defaultdate="Wed Aug 05 2020 00:00:00 GMT+0000 (Greenwich Mean Time)" displayfield="true" firstday="0" diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js index aa7aad8e93e..ae767f8b3f5 100644 --- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js +++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js @@ -153,7 +153,7 @@ describe('~/access_tokens/components/access_token_table_app', () => { let button = cells.at(6).findComponent(GlButton); expect(button.attributes()).toMatchObject({ 'aria-label': __('Revoke'), - 'data-testid': 'revoke-button', + 'data-qa-selector': __('revoke_button'), href: '/-/profile/personal_access_tokens/1/revoke', 'data-confirm': sprintf( __( diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js index 966a69fa60a..d51ac638f0e 100644 --- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js +++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js @@ -81,6 +81,20 @@ describe('~/access_tokens/components/new_access_token_app', () => { ); }); + it('input field should contain QA-related selectors', async () => { + const newToken = '12345'; + await triggerSuccess(newToken); + + expect(findGlAlertError().exists()).toBe(false); + + const inputAttributes = wrapper + .findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType })) + .attributes(); + expect(inputAttributes).toMatchObject({ + 'data-qa-selector': 'created_access_token_field', + }); + }); + it('should render an info alert', async () => { await triggerSuccess(); diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js index f14d24538d8..58c40a49b3c 100644 --- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js @@ -4,6 +4,7 @@ import InviteMembersTrigger from '~/invite_members/components/invite_members_tri import eventHub from '~/invite_members/event_hub'; import { TRIGGER_ELEMENT_BUTTON, + TRIGGER_DEFAULT_QA_SELECTOR, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, @@ -65,6 +66,18 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => { expect(findButton().text()).toBe(displayText); }); + + it('uses the default qa selector value', () => { + createComponent(); + + expect(findButton().attributes('data-qa-selector')).toBe(TRIGGER_DEFAULT_QA_SELECTOR); + }); + + it('sets the qa selector value', () => { + createComponent({ qaSelector: '_qaSelector_' }); + + expect(findButton().attributes('data-qa-selector')).toBe('_qaSelector_'); + }); }); describe('clicking the link', () => { diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js index 45d2afb0eac..e70c83a424e 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -91,6 +91,7 @@ describe('InviteModalBase', () => { const actionButton = findActionButton(); expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT); + expect(actionButton.attributes('data-qa-selector')).toBe('invite_button'); expect(actionButton.props()).toMatchObject({ variant: 'confirm', diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js index b825a578cee..53218d794c7 100644 --- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js +++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js @@ -19,7 +19,7 @@ describe('Confirm Danger Modal', () => { const findModal = () => wrapper.findComponent(GlModal); const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase'); - const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-field'); + const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input'); const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning'); const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message'); const findPrimaryAction = () => findModal().props('actionPrimary'); diff --git a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb index 0408357e8f2..48be1c29184 100644 --- a/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb +++ b/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Resolvers::ContainerRepositoryTagsResolver do +RSpec.describe Resolvers::ContainerRepositoryTagsResolver, feature_category: :container_registry do include GraphqlHelpers let_it_be(:user) { create(:user) } @@ -12,51 +12,135 @@ RSpec.describe Resolvers::ContainerRepositoryTagsResolver do let(:args) { { sort: nil } } describe '#resolve' do - let(:resolver) do - resolve( - described_class, - ctx: { current_user: user }, - obj: repository, - args: args, - arg_style: :internal - ) + shared_examples 'fetching via tags and filter in place' do + context 'by name' do + subject { resolver(args).map(&:name) } + + before do + stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false) + end + + context 'without sort' do + # order is not guaranteed + it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') } + end + + context 'with sorting and filtering' do + context 'name_asc' do + let(:args) { { sort: 'NAME_ASC' } } + + it { is_expected.to eq(%w[123 aaa bab bbb ccc]) } + end + + context 'name_desc' do + let(:args) { { sort: 'NAME_DESC' } } + + it { is_expected.to eq(%w[ccc bbb bab aaa 123]) } + end + + context 'filter by name' do + let(:args) { { sort: 'NAME_DESC', name: 'b' } } + + it { is_expected.to eq(%w[bbb bab]) } + end + end + end end before do stub_container_registry_config(enabled: true) end - context 'by name' do - subject { resolver.map(&:name) } - + context 'when Gitlab API is supported', :saas do before do - stub_container_registry_tags(repository: repository.path, tags: %w[aaa bab bbb ccc 123], with_manifest: false) + allow(repository).to receive(:tags_page).and_return({ + tags: [], + pagination: { + previous: { uri: URI('/test?before=prev-cursor') }, + next: { uri: URI('/test?last=next-cursor') } + } + }) + + allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true) end - context 'without sort' do - # order is not guaranteed - it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') } + context 'get the page size based on first and last param' do + it 'sends the page size based on first if next page is asked' do + args = { sort: 'NAME_ASC', first: 10 } + expect(repository).to receive(:tags_page).with(hash_including(page_size: args[:first])) + + resolver(args) + end + + it 'sends the page size based on last if prev page is asked' do + args = { sort: 'NAME_ASC', last: 10 } + expect(repository).to receive(:tags_page).with(hash_including(page_size: args[:last])) + + resolver(args) + end end - context 'with sorting and filtering' do - context "name_asc" do - let(:args) { { sort: :name_asc } } + context 'with parameters' do + using RSpec::Parameterized::TableSyntax - it { is_expected.to eq(%w[123 aaa bab bbb ccc]) } + where(:before, :after, :sort, :name, :first, :last, :sort_value) do + nil | nil | 'NAME_DESC' | '' | 10 | nil | '-name' + 'bb' | nil | 'NAME_ASC' | 'a' | nil | 5 | 'name' + nil | 'aa' | 'NAME_DESC' | 'a' | 10 | nil | '-name' end - context "name_desc" do - let(:args) { { sort: :name_desc } } + with_them do + let(:args) do + { before: before, after: after, sort: sort, name: name, first: first, last: last }.compact + end + + it 'calls ContainerRepository#tags_page with correct parameters' do + expect(repository).to receive(:tags_page).with( + before: before, + last: after, + sort: sort_value, + name: name, + page_size: [first, last].map(&:to_i).max + ) - it { is_expected.to eq(%w[ccc bbb bab aaa 123]) } + resolver(args) + end end + end + + it 'returns an ExternallyPaginatedArray' do + expect(Gitlab::Graphql::ExternallyPaginatedArray) + .to receive(:new).with('prev-cursor', 'next-cursor') - context 'filter by name' do - let(:args) { { sort: :name_desc, name: 'b' } } + expect(resolver(args)).is_a? Gitlab::Graphql::ExternallyPaginatedArray + end - it { is_expected.to eq(%w[bbb bab]) } + context 'when feature use_repository_list_tags_on_graphql is disabled' do + before do + stub_feature_flags(use_repository_list_tags_on_graphql: false) end + + it_behaves_like 'fetching via tags and filter in place' end end + + context 'when Gitlab API is not supported' do + before do + allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(false) + end + + it_behaves_like 'fetching via tags and filter in place' + end + + def resolver(args, opts = {}) + field_options = { + owner: resolver_parent, + resolver: described_class, + connection_extension: Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension + }.merge(opts) + + field = ::Types::BaseField.from_options('field_value', **field_options) + resolve_field(field, repository, args: args, object_type: resolver_parent) + end end end diff --git a/spec/lib/container_registry/gitlab_api_client_spec.rb b/spec/lib/container_registry/gitlab_api_client_spec.rb index 86675ba27f6..3c87af3a1c8 100644 --- a/spec/lib/container_registry/gitlab_api_client_spec.rb +++ b/spec/lib/container_registry/gitlab_api_client_spec.rb @@ -220,6 +220,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_ { name: '0.1.0', digest: 'sha256:1234567890', + config_digest: 'sha256:13828381121', media_type: 'application/vnd.oci.image.manifest.v1+json', size_bytes: 1234567890, created_at: 5.minutes.ago @@ -227,6 +228,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_ { name: 'latest', digest: 'sha256:1234567892', + config_digest: 'sha256:33139438113', media_type: 'application/vnd.oci.image.manifest.v1+json', size_bytes: 1234567892, created_at: 10.minutes.ago diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb index cb5c6a60e1d..8f9308f2127 100644 --- a/spec/lib/container_registry/tag_spec.rb +++ b/spec/lib/container_registry/tag_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ContainerRegistry::Tag do +RSpec.describe ContainerRegistry::Tag, feature_category: :container_registry do let(:group) { create(:group, name: 'group') } let(:project) { create(:project, path: 'test', group: group) } @@ -74,6 +74,77 @@ RSpec.describe ContainerRegistry::Tag do end end + describe '#total_size' do + context 'when total_size is set' do + before do + tag.total_size = 1000 + end + + it 'returns the set size' do + expect(tag.total_size).to eq(1000) + end + end + end + + describe '#revision' do + context 'when revision is set' do + before do + tag.revision = 'xyz789' + end + + it 'returns the set revision' do + expect(tag.revision).to eq('xyz789') + end + end + + context 'when revision is not set' do + context 'when config_blob is not nil' do + let(:blob) { ContainerRegistry::Blob.new(repository, {}) } + + before do + allow(tag).to receive(:config_blob).and_return(blob) + allow(blob).to receive(:revision).and_return('abc123') + end + + it 'returns the revision from config_blob' do + expect(tag.revision).to eq('abc123') + end + end + + context 'when config_blob is nil' do + before do + allow(tag).to receive(:config_blob).and_return(nil) + end + + it 'returns nil' do + expect(tag.revision).to be_nil + end + end + end + end + + describe '#short_revision' do + context 'when revision is not nil' do + before do + allow(tag).to receive(:revision).and_return('abcdef1234567890') + end + + it 'returns the first 9 characters of the revision' do + expect(tag.short_revision).to eq('abcdef123') + end + end + + context 'when revision is nil' do + before do + allow(tag).to receive(:revision).and_return(nil) + end + + it 'returns nil' do + expect(tag.short_revision).to be_nil + end + end + end + context 'schema v1' do before do stub_request(:get, 'http://registry.gitlab/v2/group/test/manifests/tag') @@ -277,6 +348,16 @@ RSpec.describe ContainerRegistry::Tag do end describe '#digest' do + context 'when manifest_digest is set' do + before do + tag.manifest_digest = 'sha256:manifestdigest' + end + + it 'returns the set manifest_digest' do + expect(tag.digest).to eq('sha256:manifestdigest') + end + end + it 'returns a correct tag digest' do expect(tag.digest).to eq 'sha256:digest' end diff --git a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb index f85a68ada15..a1d9a861355 100644 --- a/spec/lib/gitlab/discussions_diff/file_collection_spec.rb +++ b/spec/lib/gitlab/discussions_diff/file_collection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::DiscussionsDiff::FileCollection do +RSpec.describe Gitlab::DiscussionsDiff::FileCollection, :clean_gitlab_redis_shared_state do let(:merge_request) { create(:merge_request) } let!(:diff_note_a) { create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) } let!(:diff_note_b) { create(:diff_note_on_merge_request, project: merge_request.project, noteable: merge_request) } @@ -11,7 +11,18 @@ RSpec.describe Gitlab::DiscussionsDiff::FileCollection do subject { described_class.new([note_diff_file_a, note_diff_file_b]) } - describe '#load_highlight', :clean_gitlab_redis_shared_state do + describe '#load_highlight' do + it 'only takes into account for the specific diff note ids' do + file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash) + + expect(Gitlab::DiscussionsDiff::HighlightCache) + .to receive(:write_multiple) + .with({ note_diff_file_a.id => file_a_caching_content }) + .and_call_original + + subject.load_highlight(diff_note_ids: [note_diff_file_a.diff_note_id]) + end + it 'writes uncached diffs highlight' do file_a_caching_content = diff_note_a.diff_file.highlighted_diff_lines.map(&:to_hash) file_b_caching_content = diff_note_b.diff_file.highlighted_diff_lines.map(&:to_hash) diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index e3628a8a32f..027fd20462b 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -675,6 +675,101 @@ RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :cont end end + describe '#tags_page' do + let_it_be(:page_size) { 100 } + let_it_be(:before) { 'before' } + let_it_be(:last) { 'last' } + let_it_be(:sort) { '-name' } + let_it_be(:name) { 'repo' } + + subject do + repository.tags_page(before: before, last: last, sort: sort, name: name, page_size: page_size) + end + + before do + allow(repository).to receive(:migrated?).and_return(true) + end + + it 'calls GitlabApiClient#tags and passes parameters' do + allow(repository.gitlab_api_client).to receive(:tags).and_return({}) + expect(repository.gitlab_api_client).to receive(:tags).with( + repository.path, page_size: page_size, before: before, last: last, sort: sort, name: name) + + subject + end + + context 'with a call to tags' do + let_it_be(:tags_response) do + [ + { + name: '0.1.0', + digest: 'sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d6670', + config_digest: 'sha256:66b1132a0173910b01ee69583bbf2f7f1e4462c99efbe1b9ab5bf', + media_type: 'application/vnd.oci.image.manifest.v1+json', + size_bytes: 1234567890, + created_at: 5.minutes.ago, + updated_at: 5.minutes.ago + }, + { + name: 'latest', + digest: 'sha256:6c3c624b58dbbcd3c0dd82b4c53f04191247c6eebdaab7c610cf7d66709b3', + config_digest: 'sha256:66b1132a0173910b01ee694462c99efbe1b9ab5bf8083231232312', + media_type: 'application/vnd.oci.image.manifest.v1+json', + size_bytes: 1234567892, + created_at: 10.minutes.ago, + updated_at: 10.minutes.ago + } + ] + end + + let_it_be(:response_body) do + { + pagination: { + previous: { uri: URI('/test?before=prev-cursor') }, + next: { uri: URI('/test?last=next-cursor') } + }, + response_body: ::Gitlab::Json.parse(tags_response.to_json) + } + end + + before do + allow(repository.gitlab_api_client).to receive(:tags).and_return(response_body) + end + + it 'returns tags and parses the previous and next cursors' do + return_value = subject + + expect(return_value[:pagination]).to eq(response_body[:pagination]) + + return_value[:tags].each_with_index do |tag, index| + expected_revision = tags_response[index][:config_digest].to_s.split(':')[1] + + expect(tag.is_a?(ContainerRegistry::Tag)).to eq(true) + expect(tag).to have_attributes( + repository: repository, + name: tags_response[index][:name], + digest: tags_response[index][:digest], + total_size: tags_response[index][:size_bytes], + revision: expected_revision, + short_revision: expected_revision[0..8], + created_at: DateTime.rfc3339(tags_response[index][:created_at].rfc3339), + updated_at: DateTime.rfc3339(tags_response[index][:updated_at].rfc3339) + ) + end + end + end + + context 'calling on a non migrated repository' do + before do + allow(repository).to receive(:migrated?).and_return(false) + end + + it 'raises an Argument error' do + expect { repository.tags_page }.to raise_error(ArgumentError, 'not a migrated repository') + end + end + end + describe '#tags_count' do it 'returns the count of tags' do expect(repository.tags_count).to eq(1) diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb index 7faddb2384c..ae75020c768 100644 --- a/spec/models/users/credit_card_validation_spec.rb +++ b/spec/models/users/credit_card_validation_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do + include CryptoHelpers + it { is_expected.to belong_to(:user) } it { is_expected.to validate_length_of(:holder_name).is_at_most(50) } @@ -206,12 +208,12 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do context 'when last_digits has a blank value' do let(:last_digits) { ' ' } - it { expect { save_credit_card_validation }.not_to change { credit_card_validation.last_digits_hash } } + it { expect(credit_card_validation).to be_invalid } end context 'when last_digits has a value' do let(:last_digits) { 1111 } - let(:expected_last_digits_hash) { Gitlab::CryptoHelper.sha256(last_digits) } + let(:expected_last_digits_hash) { sha256(last_digits) } it 'assigns correct last_digits_hash value' do expect { save_credit_card_validation }.to change { @@ -240,7 +242,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do context 'when holder_name has a value' do let(:holder_name) { 'John Smith' } - let(:expected_holder_name_hash) { Gitlab::CryptoHelper.sha256(holder_name.downcase) } + let(:expected_holder_name_hash) { sha256(holder_name.downcase) } it 'lowercases holder_name and assigns correct holder_name_hash value' do expect { save_credit_card_validation }.to change { @@ -269,7 +271,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do context 'when network has a value' do let(:network) { 'Visa' } - let(:expected_network_hash) { Gitlab::CryptoHelper.sha256(network.downcase) } + let(:expected_network_hash) { sha256(network.downcase) } it 'lowercases network and assigns correct network_hash value' do expect { save_credit_card_validation }.to change { @@ -298,7 +300,7 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do context 'when expiration_date has a value' do let(:expiration_date) { 1.year.from_now.to_date } - let(:expected_expiration_date_hash) { Gitlab::CryptoHelper.sha256(expiration_date.to_s) } + let(:expected_expiration_date_hash) { sha256(expiration_date.to_s) } it 'assigns correct expiration_date_hash value' do expect { save_credit_card_validation }.to change { diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 118a11851dd..20277c7e27b 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -313,4 +313,124 @@ RSpec.describe 'container repository details', feature_category: :container_regi end it_behaves_like 'handling graphql network errors with the container registry' + + context 'when list tags API is enabled', :saas do + before do + stub_container_registry_config(enabled: true) + allow(ContainerRegistry::GitlabApiClient).to receive(:supports_gitlab_api?).and_return(true) + + allow_next_instances_of(ContainerRegistry::GitlabApiClient, nil) do |client| + allow(client).to receive(:tags).and_return(response_body) + end + end + + let_it_be(:raw_tags_response) do + [ + { + name: 'latest', + digest: 'sha256:1234567892', + config_digest: 'sha256:3332132331', + media_type: 'application/vnd.oci.image.manifest.v1+json', + size_bytes: 1234567892, + created_at: 10.minutes.ago, + updated_at: 10.minutes.ago + } + ] + end + + let_it_be(:url) { URI('/gitlab/v1/repositories/group1/proj1/tags/list/?before=tag1') } + + let_it_be(:response_body) do + { + pagination: { previous: { uri: url }, next: { uri: url } }, + response_body: ::Gitlab::Json.parse(raw_tags_response.to_json) + } + end + + it_behaves_like 'a working graphql query' do # OK + before do + subject + end + + it 'matches the JSON schema' do + expect(container_repository_details_response).to match_schema('graphql/container_repository_details') + end + end + + context 'with different permissions' do # OK + let_it_be(:user) { create(:user) } + + let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } + + where(:project_visibility, :role, :access_granted, :can_delete) do + :private | :maintainer | true | true + :private | :developer | true | true + :private | :reporter | true | false + :private | :guest | false | false + :private | :anonymous | false | false + :public | :maintainer | true | true + :public | :developer | true | true + :public | :reporter | true | false + :public | :guest | true | false + :public | :anonymous | true | false + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) + project.add_member(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if access_granted + expect(tags_response.size).to eq(raw_tags_response.size) + expect(container_repository_details_response.dig('canDelete')).to eq(can_delete) + else + expect(container_repository_details_response).to eq(nil) + end + end + end + end + + context 'querying' do + let(:name) { 'l' } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: name } + end + + let(:query) do + <<~GQL + query($id: ContainerRepositoryID!, $n: String) { + containerRepository(id: $id) { + tags(name: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'returns the tag response', :aggregate_failures do + subject + + expect(tags_response.size).to eq(1) + expect(tags_response.first.dig('node', 'name')).to eq('latest') + end + + context 'invalid filter' do + let(:name) { 1 } + + it_behaves_like 'returning an invalid value error' + end + end + + it_behaves_like 'handling graphql network errors with the container registry' + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 7da44266064..1abb02cd9f0 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile do include WorkhorseHelpers include KeysetPaginationHelpers + include CryptoHelpers let_it_be(:admin) { create(:admin) } let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') } @@ -1983,17 +1984,23 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile end describe "PUT /user/:id/credit_card_validation" do - let(:credit_card_validated_time) { Time.utc(2020, 1, 1) } + let(:network) { 'American Express' } + let(:holder_name) { 'John Smith' } + let(:last_digits) { '1111' } let(:expiration_year) { Date.today.year + 10 } + let(:expiration_month) { 1 } + let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) } + let(:credit_card_validated_at) { Time.utc(2020, 1, 1) } + let(:path) { "/user/#{user.id}/credit_card_validation" } let(:params) do { - credit_card_validated_at: credit_card_validated_time, + credit_card_validated_at: credit_card_validated_at, credit_card_expiration_year: expiration_year, - credit_card_expiration_month: 1, - credit_card_holder_name: 'John Smith', - credit_card_type: 'AmericanExpress', - credit_card_mask_number: '1111' + credit_card_expiration_month: expiration_month, + credit_card_holder_name: holder_name, + credit_card_type: network, + credit_card_mask_number: last_digits } end @@ -2023,11 +2030,11 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile expect(response).to have_gitlab_http_status(:ok) expect(user.credit_card_validation).to have_attributes( - credit_card_validated_at: credit_card_validated_time, - expiration_date: Date.new(expiration_year, 1, 31), - last_digits: 1111, - network: 'AmericanExpress', - holder_name: 'John Smith' + credit_card_validated_at: credit_card_validated_at, + network_hash: sha256(network.downcase), + holder_name_hash: sha256(holder_name.downcase), + last_digits_hash: sha256(last_digits), + expiration_date_hash: sha256(expiration_date.to_s) ) end diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb index 4e23b51cae2..e1c5b30115d 100644 --- a/spec/services/users/upsert_credit_card_validation_service_spec.rb +++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb @@ -3,20 +3,29 @@ require 'spec_helper' RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user_profile do + include CryptoHelpers + let_it_be(:user) { create(:user) } let(:user_id) { user.id } - let(:credit_card_validated_time) { Time.utc(2020, 1, 1) } + + let(:network) { 'American Express' } + let(:holder_name) { 'John Smith' } + let(:last_digits) { '1111' } let(:expiration_year) { Date.today.year + 10 } + let(:expiration_month) { 1 } + let(:expiration_date) { Date.new(expiration_year, expiration_month, -1) } + let(:credit_card_validated_at) { Time.utc(2020, 1, 1) } + let(:params) do { user_id: user_id, - credit_card_validated_at: credit_card_validated_time, + credit_card_validated_at: credit_card_validated_at, credit_card_expiration_year: expiration_year, - credit_card_expiration_month: 1, - credit_card_holder_name: 'John Smith', - credit_card_type: 'AmericanExpress', - credit_card_mask_number: '1111' + credit_card_expiration_month: expiration_month, + credit_card_holder_name: holder_name, + credit_card_type: network, + credit_card_mask_number: last_digits } end @@ -25,82 +34,97 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user context 'successfully set credit card validation record for the user' do context 'when user does not have credit card validation record' do - it 'creates the credit card validation and returns a success' do + it 'creates the credit card validation and returns a success', :aggregate_failures do expect(user.credit_card_validated_at).to be nil - result = service.execute + service_result = service.execute - expect(result.status).to eq(:success) + expect(service_result.status).to eq(:success) + expect(service_result.message).to eq(_('Credit card validation record saved')) user.reload expect(user.credit_card_validation).to have_attributes( - credit_card_validated_at: credit_card_validated_time, - network: 'AmericanExpress', - holder_name: 'John Smith', - last_digits: 1111, - expiration_date: Date.new(expiration_year, 1, 31) + credit_card_validated_at: credit_card_validated_at, + network_hash: sha256(network.downcase), + holder_name_hash: sha256(holder_name.downcase), + last_digits_hash: sha256(last_digits), + expiration_date_hash: sha256(expiration_date.to_s) ) end end context 'when user has credit card validation record' do - let(:old_time) { Time.utc(1999, 2, 2) } + let(:previous_credit_card_validated_at) { Time.utc(1999, 2, 2) } before do - create(:credit_card_validation, user: user, credit_card_validated_at: old_time) + create(:credit_card_validation, user: user, credit_card_validated_at: previous_credit_card_validated_at) end - it 'updates the credit card validation and returns a success' do - expect(user.credit_card_validated_at).to eq(old_time) + it 'updates the credit card validation record and returns a success', :aggregate_failures do + expect(user.credit_card_validated_at).to eq(previous_credit_card_validated_at) + + service_result = service.execute - result = service.execute + expect(service_result.status).to eq(:success) + expect(service_result.message).to eq(_('Credit card validation record saved')) - expect(result.status).to eq(:success) - expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time) + user.reload + + expect(user.credit_card_validated_at).to eq(credit_card_validated_at) end end end shared_examples 'returns an error without tracking the exception' do - it do + it 'does not send an exception to Gitlab::ErrorTracking' do expect(Gitlab::ErrorTracking).not_to receive(:track_exception) - result = service.execute + service.execute + end + + it 'returns an error', :aggregate_failures do + service_result = service.execute - expect(result.status).to eq(:error) + expect(service_result.status).to eq(:error) + expect(service_result.message).to eq(_('Error saving credit card validation record')) end end - shared_examples 'returns an error, tracking the exception' do - it do + shared_examples 'returns an error and tracks the exception' do + it 'sends an exception to Gitlab::ErrorTracking' do expect(Gitlab::ErrorTracking).to receive(:track_exception) - result = service.execute + service.execute + end + + it 'returns an error', :aggregate_failures do + service_result = service.execute - expect(result.status).to eq(:error) + expect(service_result.status).to eq(:error) + expect(service_result.message).to eq(_('Error saving credit card validation record')) end end - context 'when user id does not exist' do + context 'when the user_id does not exist' do let(:user_id) { non_existing_record_id } it_behaves_like 'returns an error without tracking the exception' end - context 'when missing credit_card_validated_at' do + context 'when the request is missing the credit_card_validated_at field' do let(:params) { { user_id: user_id } } - it_behaves_like 'returns an error, tracking the exception' + it_behaves_like 'returns an error and tracks the exception' end - context 'when missing user id' do - let(:params) { { credit_card_validated_at: credit_card_validated_time } } + context 'when the request is missing the user_id field' do + let(:params) { { credit_card_validated_at: credit_card_validated_at } } - it_behaves_like 'returns an error, tracking the exception' + it_behaves_like 'returns an error and tracks the exception' end - context 'when unexpected exception happen' do + context 'when there is an unexpected error' do let(:exception) { StandardError.new } before do @@ -109,22 +133,7 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user end end - it 'tracks the exception and returns an error' do - logged_params = { - credit_card_validated_at: credit_card_validated_time, - expiration_date: Date.new(expiration_year, 1, 31), - holder_name: "John Smith", - last_digits: 1111, - network: "AmericanExpress", - user_id: user_id - } - - expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception, class: described_class.to_s, params: logged_params) - - result = service.execute - - expect(result.status).to eq(:error) - end + it_behaves_like 'returns an error and tracks the exception' end end end diff --git a/spec/support/helpers/crypto_helpers.rb b/spec/support/helpers/crypto_helpers.rb new file mode 100644 index 00000000000..0b2d5f6386a --- /dev/null +++ b/spec/support/helpers/crypto_helpers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module CryptoHelpers + def sha256(value) + Gitlab::CryptoHelper.sha256(value) + end +end diff --git a/vite.config.js b/vite.config.js index b0e4392a9dc..9bbfc37903f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -68,6 +68,10 @@ export default defineConfig({ find: '~/', replacement: javascriptsPath, }, + { + find: '~katex', + replacement: 'katex', + }, ], }, plugins: [ |