diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs index 2f9eea46947..f8ddb0cfd41 100644 --- a/.eslint_todo/vue-no-unused-properties.mjs +++ b/.eslint_todo/vue-no-unused-properties.mjs @@ -313,9 +313,6 @@ export default { 'ee/app/assets/javascripts/projects/components/move_personal_project_to_group_modal.vue', 'ee/app/assets/javascripts/projects/merge_requests/blocking_mr_input_root.vue', 'ee/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue', - 'ee/app/assets/javascripts/requirements/components/export_requirements_modal.vue', - 'ee/app/assets/javascripts/requirements/components/import_requirements_modal.vue', - 'ee/app/assets/javascripts/requirements/components/requirements_root.vue', 'ee/app/assets/javascripts/roadmap/components/current_day_indicator.vue', 'ee/app/assets/javascripts/roadmap/components/epic_item.vue', 'ee/app/assets/javascripts/roadmap/components/epic_item_details.vue', diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index faea698c0c4..67f650fe69e 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -582,6 +582,7 @@ lib/gitlab/checks/** /doc/administration/application_settings_cache.md @jglassman1 /doc/administration/auditor_users.md @idurham /doc/administration/auth/ @idurham +/doc/administration/backup_restore/ @axil /doc/administration/broadcast_messages.md @sselhorn /doc/administration/cells.md @emily.sahlani /doc/administration/cicd/ @lyspin @@ -658,7 +659,7 @@ lib/gitlab/checks/** /doc/administration/raketasks/tokens/ @idurham /doc/administration/raketasks/x509_signatures.md @brendan777 /doc/administration/read_only_gitlab.md @axil @eread -/doc/administration/redis/ @axil @eread +/doc/administration/redis/ @axil /doc/administration/reference_architectures/ @axil @eread /doc/administration/reply_by_email.md @lciutacu /doc/administration/reply_by_email_postfix_setup.md @axil @eread @@ -704,8 +705,7 @@ lib/gitlab/checks/** /doc/administration/settings/third_party_offers.md @phillipwells /doc/administration/settings/visibility_and_access_controls.md @brendan777 /doc/administration/settings/vscode_extension_marketplace.md @brendan777 -/doc/administration/sidekiq/ @axil @eread -/doc/administration/sidekiq/sidekiq_memory_killer.md @jglassman1 +/doc/administration/sidekiq/ @axil /doc/administration/silent_mode/ @axil /doc/administration/smime_signing_email.md @axil @eread /doc/administration/snippets/ @brendan777 @@ -953,6 +953,7 @@ lib/gitlab/checks/** /doc/development/cascading_settings.md @gitlab-org/foundations/engineering /doc/development/cells/ @OmarQunsulGitlab @bmarjanovic /doc/development/cicd/ @gitlab-org/maintainers/cicd-verify +/doc/development/compromised_password_detection.md @gitlab-org/software-supply-chain-security/authentication/approvers /doc/development/contributing/verify/ @gitlab-org/maintainers/cicd-verify /doc/development/database/ @OmarQunsulGitlab @bmarjanovic /doc/development/distributed_tracing.md @gitlab-org/analytics-section/product-analytics/engineers/frontend diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index e15b4120a03..fde30dc5ea1 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -74,6 +74,8 @@ compile-production-assets as-if-foss: - .qa:rules:test-on-omnibus-ce:follow-up compile-test-assets: + variables: + BABEL_ENV: "$BABEL_ENV" extends: - .compile-assets-base - .frontend:rules:compile-test-assets diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 6bac4083910..ddd6e7b3ab1 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -2025,6 +2025,8 @@ variables: <<: *qa-e2e-test-schedule-variables COVERBAND_ENABLED: "true" + BABEL_ENV: "istanbul" # This sets the environmental variable enabling `istanbul` plugin in Babel + # https://gitlab.com/gitlab-org/gitlab/-/blob/master/babel.config.js QA_RUN_IN_PARALLEL: "false" .qa:rules:e2e:test-on-cng: diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml index 686d01d16b7..594c7481eea 100644 --- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml +++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml @@ -56,6 +56,7 @@ workflow: job: clone-gitlab-repo variables: COVERBAND_ENABLED: "$COVERBAND_ENABLED" # explicitly define variable so it is passed in to gdk service container + BABEL_ENV: "$BABEL_ENV" GITLAB_CRON_JOBS_POLL_INTERVAL: "0" GITLAB_DEVELOPMENT_USE_PRECOMPILED_ASSETS: "true" GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js index 9c6a5958e1f..c01ebb0d1d1 100644 --- a/app/assets/javascripts/blob/line_highlighter.js +++ b/app/assets/javascripts/blob/line_highlighter.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */ import { scrollToElement } from '~/lib/utils/common_utils'; +import { updateHash } from '~/blob/state'; // LineHighlighter // @@ -174,6 +175,7 @@ LineHighlighter.prototype.setHash = function (firstLineNumber, lastLineNumber) { hash = `#L${firstLineNumber}`; } this._hash = hash; + updateHash(hash); return this.__setLocationHash__(hash); }; diff --git a/app/assets/javascripts/blob/state.js b/app/assets/javascripts/blob/state.js index 2f71927f17d..f78cd4508a3 100644 --- a/app/assets/javascripts/blob/state.js +++ b/app/assets/javascripts/blob/state.js @@ -4,6 +4,6 @@ export const hashState = Vue.observable({ currentHash: window.location.hash, }); -export const updateLineNumber = (lineNumber) => { - hashState.currentHash = lineNumber; +export const updateHash = (newHash) => { + hashState.currentHash = newHash; }; diff --git a/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue b/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue index 9dbf5fac7be..fec938c55d3 100644 --- a/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue +++ b/app/assets/javascripts/repository/components/header_area/permalink_dropdown_item.vue @@ -6,9 +6,8 @@ import { InternalEvents } from '~/tracking'; import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings'; import { Mousetrap } from '~/lib/mousetrap'; import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; -import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility'; -import { hashState } from '~/blob/state'; -import { getPageParamValue, getPageSearchString } from '~/blob/utils'; +import { hashState, updateHash } from '~/blob/state'; +import { getAbsolutePermalinkPath } from './utils'; Vue.use(GlToast); @@ -41,26 +40,22 @@ export default { return shouldDisableShortcuts(); }, absolutePermalinkPath() { - const baseAbsolutePath = relativePathToAbsolute(this.permalinkPath, getBaseURL()); - if (hashState.currentHash) { - const page = getPageParamValue(hashState.currentHash); - const searchString = getPageSearchString(baseAbsolutePath, page); - if (Number.isNaN(Number(hashState.currentHash))) { - return `${baseAbsolutePath}${searchString}${hashState.currentHash}`; - } - return `${baseAbsolutePath}${searchString}#L${hashState.currentHash}`; - } - return baseAbsolutePath; + return getAbsolutePermalinkPath(this.permalinkPath, hashState.currentHash); }, }, mounted() { this.mousetrap = new Mousetrap(); this.mousetrap.bind(keysFor(PROJECT_FILES_GO_TO_PERMALINK), this.triggerCopyPermalink); + window.addEventListener('hashchange', this.onHashChange); }, beforeDestroy() { this.mousetrap.unbind(keysFor(PROJECT_FILES_GO_TO_PERMALINK)); + window.removeEventListener('hashchange', this.onHashChange); }, methods: { + onHashChange() { + updateHash(window.location.hash || ''); + }, triggerCopyPermalink() { const buttonElement = this.$refs.copyPermalinkButton.$el; buttonElement.click(); diff --git a/app/assets/javascripts/repository/components/header_area/utils.js b/app/assets/javascripts/repository/components/header_area/utils.js new file mode 100644 index 00000000000..f86a2a9e344 --- /dev/null +++ b/app/assets/javascripts/repository/components/header_area/utils.js @@ -0,0 +1,25 @@ +import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility'; +import { getPageParamValue, getPageSearchString } from '~/blob/utils'; + +/** + * Generates an absolute permalink path with proper handling of URL hash + * + * @param {String} permalinkPath - The relative permalink path + * @param {String} hash - The URL hash (after #) + * @returns {String} - The absolute permalink path with hash handling + */ +export const getAbsolutePermalinkPath = (permalinkPath, inputHash) => { + const baseAbsolutePath = relativePathToAbsolute(permalinkPath, getBaseURL()); + + const hash = inputHash || ''; + + const page = getPageParamValue(hash); + const searchString = getPageSearchString(baseAbsolutePath, page); + + // Ensure hash starts with # if it doesn't already + let normalizedHash = ''; + if (hash.trim()) { + normalizedHash = hash.startsWith('#') ? hash : `#${hash}`; + } + return `${baseAbsolutePath}${searchString}${normalizedHash}`; +}; diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue index d047efae83f..b051574f032 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue @@ -6,7 +6,6 @@ import { GlIntersectionObserver } from '@gitlab/ui'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { getPageParamValue, getPageSearchString } from '~/blob/utils'; import { addInteractionClass } from '~/code_navigation/utils'; -import { updateLineNumber } from '~/blob/state'; /* * We only highlight the chunk that is currently visible to the user. @@ -105,10 +104,6 @@ export default { this.hasAppeared = true; this.$emit('appear'); }, - handleOnClick(event) { - const { lineNumber } = event.target.dataset; - updateLineNumber(lineNumber); - }, calculateLineNumber(index) { return this.startingFrom + index + 1; }, @@ -148,7 +143,6 @@ export default { class="file-line-num gl-select-none !gl-shadow-none" :href="`#L${calculateLineNumber(index)}`" :data-line-number="calculateLineNumber(index)" - @click="handleOnClick" > {{ calculateLineNumber(index) }} diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index fc5335d2016..2300d690fb9 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -7,10 +7,9 @@ = render 'shared/new_project_item_vue_select' - if @milestone_states.any? { |name, count| count > 0 } - .top-area + .gl-flex.gl-flex-wrap.gl-items-center.gl-flex-wrap-reverse.gl-border-b = render 'shared/milestones_filter', counts: @milestone_states - .nav-controls - = render 'shared/milestones/search_form' + = render 'shared/milestones/search_form' - if @milestones.blank? = render 'shared/empty_states/milestones_tab', active_tab: params[:state] do diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 8c181da4bc9..7bfe03bb0e6 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -41,6 +41,7 @@ = dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert" = dispensable_render_if_exists "shared/silent_mode_banner" = dispensable_render_if_exists "shared/pipl_compliance_alert" + = dispensable_render_if_exists "shared/compromised_password_detection_alert" = yield :page_level_alert -# Top bar diff --git a/babel.config.js b/babel.config.js index 4c7711dd4d6..d135d2f10be 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,7 @@ const coreJSVersion = require('./node_modules/core-js/package.json').version; +console.debug(`BABEL_ENV inside Babel config is: ${process.env.BABEL_ENV}`); + let presets = [ [ '@babel/preset-env', diff --git a/config/feature_flags/beta/revalidate_gpg_fingerprints.yml b/config/feature_flags/beta/revalidate_gpg_fingerprints.yml deleted file mode 100644 index d9752597d67..00000000000 --- a/config/feature_flags/beta/revalidate_gpg_fingerprints.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: revalidate_gpg_fingerprints -feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349505 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182738 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/534717 -milestone: '17.11' -group: group::source code -type: beta -default_enabled: true diff --git a/config/webpack.config.js b/config/webpack.config.js index 651e7362ac1..72671ef7048 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -89,6 +89,8 @@ if (WEBPACK_REPORT) { NO_HASHED_CHUNKS = true; } +console.debug(`BABEL_ENV inside Webpack is: ${process.env.BABEL_ENV}`); + const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map'; const incrementalCompiler = createIncrementalWebpackCompiler( diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 7a4055b9f89..d2bcf6af2b0 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -2068,6 +2068,7 @@ Input type: `AiActionInput` | `platformOrigin` | [`String`](#string) | Specifies the origin platform of the request. | | `projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. | | `resolveVulnerability` | [`AiResolveVulnerabilityInput`](#airesolvevulnerabilityinput) | Input for resolve_vulnerability AI action. | +| `rootNamespaceId` | [`NamespaceID`](#namespaceid) | Global ID of the top-level namespace the user is acting on. | | `summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. | | `summarizeNewMergeRequest` | [`AiSummarizeNewMergeRequestInput`](#aisummarizenewmergerequestinput) | Input for summarize_new_merge_request AI action. | | `summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. | diff --git a/doc/development/duo_workflow/_index.md b/doc/development/duo_workflow/_index.md index e849e6b92e3..53a45649d96 100644 --- a/doc/development/duo_workflow/_index.md +++ b/doc/development/duo_workflow/_index.md @@ -37,3 +37,39 @@ There is no need to set up the backend components of Duo Workflow to test change A local build of the UI is required if you are making Duo Workflow UI changes that you need to view locally. A local build is also required if you want to use a version of the UI that has not been released yet. Refer to the [GitLab Duo Workflow README](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/blob/main/packages/webview_duo_workflow/README.md) file in the Language Server project to get started with local development of GitLab Duo Workflow UI. + +## Development settings + +Each of these settings can be turned on in your user settings in VS Code. + +### Change view type + +Enable the Duo Workflow as a sidepanel instead of fullview. This is going to be the default for public beta. + +`"gitlab.featureFlags.duoWorkflowPanel": true,` + +### Executor type + +Allow to define which Duo Workflow executor is selected. Accepts: + +- `shell` - Current default, runs the go binary directly on the user's machine +- `docker` - Runs the go binary inside a Docker container (deprecated) +- `node` - Runs a nodeJs/TypeScript executor directly inside the languge server. Expected to become the default. + +`"gitlab.duo.workflow.executor": "node",` + +### Workflow graph + +Experimental settings that allow Duo Workflow graph to be swapped. Includes: + +- `software_development` - default +- `chat` - used by agentic chat +- `search_and_replace` - Used to scan large number of files and replace results with specific instructions + +`"gitlab.duo.workflow.graph": "software_development",` + +### Tool approval + +Allow users to get access to tools that require approval such as running terminal commands. + +`"gitlab.duo.workflow.toolApproval": true` diff --git a/doc/development/testing_guide/end_to_end/troubleshooting.md b/doc/development/testing_guide/end_to_end/troubleshooting.md index e5402c7b733..5b894badb4f 100644 --- a/doc/development/testing_guide/end_to_end/troubleshooting.md +++ b/doc/development/testing_guide/end_to_end/troubleshooting.md @@ -66,3 +66,23 @@ For example, if your IP is `192.168.0.12`: ```shell bundle exec bin/qa Test::Instance::All http://192.168.0.12:3000 ``` + +## Tests sign out when visiting a page + +If the tests sign in successfully as a test user, but then unexpectedly sign out, you might be using an +incorrect URL to execute the test. By default, tests use the URL `http://127.0.0.1:3000`, but if a hostname +has been configured for the instance, you must explicitly pass that hostname to the tests. The tests use +the `web_url` returned by the API to go to different pages. They go to the configured hostname, rather than +`http://127.0.0.1:3000`, so the test user appears signed out. + +This example runs the tests against `http://127.0.0.1:3000`, and signs out if a hostname has been configured: + +```shell +bundle exec rspec qa/specs/features/ee/browser_ui/3_create/repository/code_owners_spec.rb +``` + +To avoid this, explicitly set `QA_GITLAB_URL` to the configured hostname, for example: + +```shell +QA_GITLAB_URL=http://gdk.test:3000 bundle exec rspec qa/specs/features/ee/browser_ui/3_create/repository/code_owners_spec.rb +``` diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index 296177aac68..f8c8f0ae71d 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -9,16 +9,6 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def run - unless Feature.enabled?(:revalidate_gpg_fingerprints, @gpg_key.user) - return CommitSignatures::GpgSignature - .select(:id, :commit_sha, :project_id) - .where('gpg_key_id IS NULL OR verification_status <> ?', CommitSignatures::GpgSignature.verification_statuses[:verified]) - .where(gpg_key_primary_keyid: @gpg_key.keyids) - .find_each do |sig| - sig.gpg_commit&.update_signature!(sig) - end - end - [@gpg_key].concat(@gpg_key.subkeys).each do |key| Gitlab::Gpg.using_tmp_keychain do Gitlab::Gpg::CurrentKeyChain.add(key.key) diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index 35c2956f230..ff30a606d17 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -42,7 +42,7 @@ namespace :tw do # CodeOwnerRule.new('Database Operations', ''), # CodeOwnerRule.new('DataOps', ''), # CodeOwnerRule.new('Delivery', ''), - # CodeOwnerRule.new('Durability', ''), + CodeOwnerRule.new('Durability', '@axil'), CodeOwnerRule.new('Duo Chat', '@jglassman1'), CodeOwnerRule.new('Duo Workflow', '@sselhorn'), CodeOwnerRule.new('Dynamic Analysis', '@phillipwells'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6ae362a5691..729dfeb927d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16748,6 +16748,42 @@ msgstr "" msgid "Components must have a 'name'" msgstr "" +msgid "CompromisedPasswordDetection|Change GitLab Password" +msgstr "" + +msgid "CompromisedPasswordDetection|Failure to change your password may lead to temporary account access restrictions to prevent potential unauthorized access." +msgstr "" + +msgid "CompromisedPasswordDetection|For your security, we also recommend enabling %{mfa_link} if you have not done so already." +msgstr "" + +msgid "CompromisedPasswordDetection|GitLab documentation" +msgstr "" + +msgid "CompromisedPasswordDetection|Instructions for changing your password can be found in the %{doc_link}." +msgstr "" + +msgid "CompromisedPasswordDetection|Please change your password immediately." +msgstr "" + +msgid "CompromisedPasswordDetection|Security Alert: Change Your GitLab.com Password" +msgstr "" + +msgid "CompromisedPasswordDetection|Security Alert: Change your GitLab password" +msgstr "" + +msgid "CompromisedPasswordDetection|The password used for your GitLab.com account %{code_start}%{username}%{code_end} may have been compromised due to a data breach on another service or platform. This does not necessarily mean that your GitLab account was accessed. However, leaving your password unchanged presents a significant security risk to your account." +msgstr "" + +msgid "CompromisedPasswordDetection|The password used for your GitLab.com account %{username} may have been compromised due to a data breach on another service or platform. This does not necessarily mean that your GitLab account was accessed. However, leaving your password unchanged presents a significant security risk to your account." +msgstr "" + +msgid "CompromisedPasswordDetection|Your GitLab.com account password may be compromised due to a data breach on another service or platform. Please change your password immediately." +msgstr "" + +msgid "CompromisedPasswordDetection|two-factor authentication" +msgstr "" + msgid "Compute minutes" msgstr "" @@ -27745,9 +27781,6 @@ msgstr "" msgid "Geo|Resync all" msgstr "" -msgid "Geo|Retry count" -msgstr "" - msgid "Geo|Reverify" msgstr "" @@ -27793,9 +27826,6 @@ msgstr "" msgid "Geo|Shards to synchronize" msgstr "" -msgid "Geo|Show more" -msgstr "" - msgid "Geo|Site name can't be blank" msgstr "" @@ -27829,9 +27859,6 @@ msgstr "" msgid "Geo|Synchronization" msgstr "" -msgid "Geo|Synchronization failed - %{error}" -msgstr "" - msgid "Geo|Synchronization settings" msgstr "" @@ -27928,9 +27955,6 @@ msgstr "" msgid "Geo|Verification concurrency limit" msgstr "" -msgid "Geo|Verification failed - %{error}" -msgstr "" - msgid "Geo|Verification information" msgstr "" @@ -28195,6 +28219,9 @@ msgstr "" msgid "GitLab Premium" msgstr "" +msgid "GitLab Security" +msgstr "" + msgid "GitLab Shell" msgstr "" @@ -61139,6 +61166,9 @@ msgstr "" msgid "Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can." msgstr "" +msgid "Thank you," +msgstr "" + msgid "Thanks for signing up to GitLab!" msgstr "" diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb index ae81e3a5734..5f436f9259e 100644 --- a/qa/qa/specs/spec_helper.rb +++ b/qa/qa/specs/spec_helper.rb @@ -72,7 +72,7 @@ RSpec.configure do |config| begin Capybara.current_session.execute_script("window.__coveragePathsPersistence.reset()") rescue StandardError => e - QA::Runtime::Logger.warn("Failed to reset coverage paths: #{e.message}") + QA::Runtime::Logger.warn("Failed to reset coverage paths, check if it is an api spec: #{e.message}") end end @@ -111,7 +111,7 @@ RSpec.configure do |config| example.metadata[:coverage_paths] = coverage_paths front_end_coverage_by_example[example.metadata[:location]] = coverage_paths rescue StandardError => e - QA::Runtime::Logger.warn("Failed to collect coverage paths: #{e.message}") + QA::Runtime::Logger.warn("Failed to collect coverage paths, check if it is an api spec: #{e.message}") end end end diff --git a/qa/qa/tools/ci/code_paths_mapping.rb b/qa/qa/tools/ci/code_paths_mapping.rb index 1496c5a4481..e705e878323 100644 --- a/qa/qa/tools/ci/code_paths_mapping.rb +++ b/qa/qa/tools/ci/code_paths_mapping.rb @@ -9,17 +9,24 @@ module QA include Helpers PROJECT = "gitlab-qa-resources" - BUCKET = "code-path-mappings" + DEFAULT_BUCKET = "code-path-mappings" + DEFAULT_FILE_NAME = "test-code-paths-mapping-merged-pipeline" - def self.export(mapping_files_glob) - new.export(mapping_files_glob) + def self.export(mapping_files_glob, **kwargs) + if kwargs.key?(:bucket) || kwargs.key?(:file_name) + new.export(mapping_files_glob, **kwargs) + else + new.export(mapping_files_glob) + end end # Export code path mappings to GCP # # @param [String] mapping_files_glob - glob pattern for mapping files + # @param [String] bucket - custom bucket name (optional) + # @param [String] file_name - custom file name (optional) # @return [void] - def export(mapping_files_glob) + def export(mapping_files_glob, bucket: DEFAULT_BUCKET, file_name: DEFAULT_FILE_NAME) mapping_files = Dir.glob(mapping_files_glob) return logger.warn("No files matched pattern, skipping coverage mapping upload") if mapping_files.empty? @@ -30,21 +37,25 @@ module QA logger.info("Number of mapping files found: #{mapping_files.size}") mapping_data = mapping_files.flat_map { |file| JSON.parse(File.read(file)) }.reduce(:merge!) - file = "#{ENV['CI_COMMIT_REF_SLUG']}/#{ENV['QA_RUN_TYPE']}/test-code-paths-mapping-merged-pipeline-#{\ + file = "#{ENV['CI_COMMIT_REF_SLUG']}/#{ENV['QA_RUN_TYPE']}/#{file_name}-#{\ ENV['CI_PIPELINE_ID'] || 'local'}.json" - upload_to_gcs(file, mapping_data) + upload_to_gcs(file, mapping_data, bucket) end # Import code path mappings from GCP # # @param [String] branch - branch name # @param [String] run_type - run type + # @param [String] bucket - custom bucket name (optional) + # @param [String] file_name - custom file name base (optional) # @return [Hash] - def import(branch, run_type) - filename = code_paths_mapping_file("#{branch}/#{run_type}") + def import(branch, run_type, bucket: DEFAULT_BUCKET, file_name: DEFAULT_FILE_NAME) + prefix = "#{branch}/#{run_type}/#{file_name}" + + filename = code_paths_mapping_file(prefix, bucket) logger.info("The mapping file fetched in import: #{filename}") - file = client.get_object(BUCKET, filename) + file = client.get_object(bucket, filename) JSON.parse(file[:body]) rescue StandardError => e logger.error("Failed to download code paths mapping from GCS. Error: #{e}") @@ -54,8 +65,9 @@ module QA private - def upload_to_gcs(file_name, mapping_data) - client.put_object(BUCKET, file_name, JSON.pretty_generate(mapping_data)) + def upload_to_gcs(file_name, mapping_data, bucket) + client.put_object(bucket, file_name, JSON.pretty_generate(mapping_data)) + logger.info("Successfully uploaded to bucket: #{bucket}, file: #{file_name}") rescue StandardError => e logger.error("Failed to upload code paths mapping to GCS. Error: #{e}") logger.error("Backtrace: #{e.backtrace}") @@ -85,8 +97,8 @@ module QA # # Get most up to date mapping file based on pipeline type # @return [String] - def code_paths_mapping_file(prefix) - paginated_list(client.list_objects(BUCKET, prefix: prefix)).last&.name + def code_paths_mapping_file(prefix, bucket = DEFAULT_BUCKET) + paginated_list(client.list_objects(bucket, prefix: prefix)).last&.name end # Paginated list of items @@ -98,7 +110,7 @@ module QA return list.items if list.next_page_token.nil? paginated_list( - client.list_objects(BUCKET, prefix: list.prefixes.first, page_token: list.next_page_token) + client.list_objects(list.bucket, prefix: list.prefixes.first, page_token: list.next_page_token) ) + list.items end end diff --git a/qa/spec/tools/ci/code_paths_mapping_spec.rb b/qa/spec/tools/ci/code_paths_mapping_spec.rb index 7715ab47abd..dc8f544b14e 100644 --- a/qa/spec/tools/ci/code_paths_mapping_spec.rb +++ b/qa/spec/tools/ci/code_paths_mapping_spec.rb @@ -51,6 +51,29 @@ RSpec.describe QA::Tools::Ci::CodePathsMapping do end end + context "with bucket and file name prefix passed as arguments" do + let(:new_bucket) { "new_bucket" } + let(:upload_file_name) { "new_name_prefix" } + let(:expected_file_path) { "#{commit_ref}/#{run_type}/#{upload_file_name}-#{ENV['CI_PIPELINE_ID']}.json" } + + it "exports mapping json with correct file name prefix" do + expect(logger).to receive(:info).with("Number of mapping files found: #{file_paths.size}") + expect(gcs_client).to receive(:put_object).with(new_bucket, expected_file_path, pretty_generated_mapping_json) + described_class.export(glob, bucket: new_bucket, file_name: upload_file_name) + end + end + + context "with bucket and file name prefix not passed as arguments" do + let(:default_bucket) { QA::Tools::Ci::CodePathsMapping::DEFAULT_BUCKET } + let(:default_filename) { QA::Tools::Ci::CodePathsMapping::DEFAULT_FILE_NAME } + let(:expected_file_path) { "#{commit_ref}/#{run_type}/#{default_filename}-#{ENV['CI_PIPELINE_ID']}.json" } + + it "exports mapping json with default file name prefix to default bucket" do + expect(gcs_client).to receive(:put_object).with(default_bucket, expected_file_path, pretty_generated_mapping_json) + described_class.export(glob) + end + end + context "with no mapping files present" do let(:file_paths) { [] } diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake index 3e371153be1..f43e4676f6e 100644 --- a/qa/tasks/ci.rake +++ b/qa/tasks/ci.rake @@ -103,10 +103,18 @@ namespace :ci do QA::Tools::Ci::TestMetrics.export(args[:glob]) end - desc "Export code paths mapping to GCP" + desc "Export backend code paths mapping to GCP" task :export_code_paths_mapping, [:glob] do |_, args| raise("Code paths mapping JSON glob pattern is required") unless args[:glob] QA::Tools::Ci::CodePathsMapping.export(args[:glob]) end + + desc "Export frontend code paths mapping to GCP" + task :export_code_paths_mapping, [:glob] do |_, args| + raise("Code paths mapping JSON glob pattern is required") unless args[:glob] + + QA::Tools::Ci::CodePathsMapping.export(args[:glob], bucket: "code-path-mappings", + file_name: "js-coverage-by-example-merged-pipeline") + end end diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js index c7a86d6230a..4250a0610ad 100644 --- a/spec/frontend/blob/line_highlighter_spec.js +++ b/spec/frontend/blob/line_highlighter_spec.js @@ -2,8 +2,11 @@ import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import LineHighlighter from '~/blob/line_highlighter'; +import { updateHash } from '~/blob/state'; import * as utils from '~/lib/utils/common_utils'; +jest.mock('~/blob/state'); + describe('LineHighlighter', () => { const testContext = {}; @@ -283,5 +286,17 @@ describe('LineHighlighter', () => { expect(testContext.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); }); + + it('calls updateHash with the correct hash for a single line', () => { + testContext.subject(5); + + expect(updateHash).toHaveBeenCalledWith('#L5'); + }); + + it('calls updateHash with the correct hash for a range', () => { + testContext.subject(5, 15); + + expect(updateHash).toHaveBeenCalledWith('#L5-15'); + }); }); }); diff --git a/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js b/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js index a52f22e543d..7d661f19ac6 100644 --- a/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js +++ b/spec/frontend/repository/components/header_area/blob_repository_actions_group_spec.js @@ -9,7 +9,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { blobControlsDataMock } from 'ee_else_ce_jest/repository/mock_data'; jest.mock('~/behaviors/shortcuts/shortcuts_toggle'); -jest.mock('~/blob/state'); const relativePermalinkPath = 'flightjs/Flight/-/blob/46ca9ebd5a43ec240ee8d64e2bb829169dff744e/bower.json'; diff --git a/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js b/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js index f548546fa64..19b5032704c 100644 --- a/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js +++ b/spec/frontend/repository/components/header_area/permalink_dropdown_item_spec.js @@ -4,7 +4,7 @@ import PermalinkDropdownItem from '~/repository/components/header_area/permalink import { keysFor, PROJECT_FILES_GO_TO_PERMALINK } from '~/behaviors/shortcuts/keybindings'; import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle'; import { Mousetrap } from '~/lib/mousetrap'; -import { hashState } from '~/blob/state'; +import { hashState, updateHash } from '~/blob/state'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper'; @@ -46,6 +46,24 @@ describe('PermalinkDropdownItem', () => { expect(findPermalinkLinkDropdown().exists()).toBe(true); }); + describe('hash change handling', () => { + it('calls updateHash when hash changes', () => { + window.location.hash = 'L42'; + createComponent(); + window.dispatchEvent(new Event('hashchange')); + + expect(updateHash).toHaveBeenCalledWith('#L42'); + }); + + it('handles empty hash correctly', () => { + window.location.hash = ''; + createComponent(); + window.dispatchEvent(new Event('hashchange')); + + expect(updateHash).toHaveBeenCalledWith(''); + }); + }); + describe('updatedPermalinkPath', () => { it('returns absolutePermalinkPath when no line number is set', () => { expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe( @@ -54,7 +72,7 @@ describe('PermalinkDropdownItem', () => { }); it('returns updated path with line number when set', () => { - hashState.currentHash = 10; + hashState.currentHash = '#L10'; createComponent(); expect(findPermalinkLinkDropdown().attributes('data-clipboard-text')).toBe( @@ -158,6 +176,17 @@ describe('PermalinkDropdownItem', () => { wrapper.destroy(); expect(unbindSpy).toHaveBeenCalledWith(keysFor(PROJECT_FILES_GO_TO_PERMALINK)); }); + + it('add and remove event listener for hashChange event', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + createComponent(); + expect(addEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function)); + + wrapper.destroy(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('hashchange', expect.any(Function)); + }); }); it('displays the shortcut key when shortcuts are not disabled', () => { diff --git a/spec/frontend/repository/components/header_area/utils_spec.js b/spec/frontend/repository/components/header_area/utils_spec.js new file mode 100644 index 00000000000..b05a2d48036 --- /dev/null +++ b/spec/frontend/repository/components/header_area/utils_spec.js @@ -0,0 +1,75 @@ +import { getAbsolutePermalinkPath } from '~/repository/components/header_area/utils'; +import * as urlUtility from '~/lib/utils/url_utility'; +import * as blobUtils from '~/blob/utils'; + +describe('getAbsolutePermalinkPath', () => { + const permalinkPath = '/project/repo/-/blob/main/file.js'; + const baseUrl = 'https://gitlab.example.com'; + const absolutePath = 'https://gitlab.example.com/project/repo/-/blob/main/file.js'; + + beforeEach(() => { + jest.spyOn(urlUtility, 'getBaseURL').mockReturnValue(baseUrl); + jest.spyOn(urlUtility, 'relativePathToAbsolute').mockReturnValue(absolutePath); + jest.spyOn(blobUtils, 'getPageParamValue').mockReturnValue(null); + jest.spyOn(blobUtils, 'getPageSearchString').mockReturnValue(''); + }); + + describe('when hash is not provided', () => { + it.each([ + ['null', null], + ['empty string', ''], + ['undefined', undefined], + ])('returns absolute path when hash is %s', (_, hash) => { + expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(absolutePath); + }); + }); + + describe('when handling different hash formats', () => { + it.each([ + ['line number format', '#L6', '#L6'], + ['line number range format', '#L10-19', '#L10-19'], + [ + 'anchor hash', + '#developer-certificate-of-origin--license', + '#developer-certificate-of-origin--license', + ], + ])('handles %s (%s)', (_, hash, expectedHash) => { + expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(`${absolutePath}${expectedHash}`); + }); + }); + + describe('when hash normalization is needed', () => { + it.each([ + ['line number', 'L6', '#L6'], + ['line number range', 'L10-19', '#L10-19'], + [ + 'complex anchor', + 'developer-certificate-of-origin--license', + '#developer-certificate-of-origin--license', + ], + ])('normalizes %s hash by adding # prefix when missing', (_, hash, expectedHash) => { + expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe(`${absolutePath}${expectedHash}`); + }); + }); + + describe('when page parameters are present', () => { + const searchString = '?plain=1'; + + beforeEach(() => { + blobUtils.getPageParamValue.mockReturnValue('1'); + blobUtils.getPageSearchString.mockReturnValue(searchString); + }); + + it.each([ + ['with # prefix', '#L6', '#L6'], + ['without # prefix', 'L20', '#L20'], + ['with empty hash', '', ''], + ['with null hash', null, ''], + ['with undefined hash', undefined, ''], + ])('includes search string when hash is %s', (_, hash, expectedHash) => { + expect(getAbsolutePermalinkPath(permalinkPath, hash)).toBe( + `${absolutePath}${searchString}${expectedHash}`, + ); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js index 46bb7216dde..0ee6dccdff4 100644 --- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js @@ -5,11 +5,9 @@ import { GlIntersectionObserver } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue'; import { addInteractionClass } from '~/code_navigation/utils'; -import { updateLineNumber } from '~/blob/state'; import { CHUNK_1, CHUNK_2 } from '../mock_data'; jest.mock('~/code_navigation/utils'); -jest.mock('~/blob/state'); Vue.use(Vuex); @@ -103,18 +101,5 @@ describe('Chunk component', () => { expect(addInteractionClass).toHaveBeenCalledWith({ d: 'test', path: 'index.js' }); }); - - it('calls updateLineNumber with the correct line number when a line is clicked', async () => { - createComponent({ ...CHUNK_2, isHighlighted: true }); - - const lineNumber = '71'; - const lineNumberElement = wrapper.find(`[data-line-number="${lineNumber}"]`); - - expect(lineNumberElement.exists()).toBe(true); - - await lineNumberElement.trigger('click'); - - expect(updateLineNumber).toHaveBeenCalledWith(lineNumber); - }); }); }); diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index d034644ceeb..6e248cd512e 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -67,23 +67,6 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater, :sidekiq_inline do ) end - it 'assigns the gpg key even with revalidate_gpg_fingerprints feature flag disabled' do - stub_feature_flags(revalidate_gpg_fingerprints: false) - - # InvalidGpgSignatureUpdater is called by the after_create hook - gpg_key = create :gpg_key, - key: GpgHelpers::User1.public_key, - user: user - - expect(valid_gpg_signature.reload).to have_attributes( - project: project, - commit_sha: commit_sha, - gpg_key: gpg_key, - gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - verification_status: 'verified' - ) - end - it 'does not assign the gpg key when an unrelated gpg key is added' do # InvalidGpgSignatureUpdater is called by the after_create hook create :gpg_key, @@ -127,23 +110,6 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater, :sidekiq_inline do ) end - it 'does not update the signature when revalidate_gpg_fingerprints feature flag is disabled' do - stub_feature_flags(revalidate_gpg_fingerprints: false) - - # InvalidGpgSignatureUpdater is called by the after_create hook - create :gpg_key, - key: GpgHelpers::User1.public_key, - user: user - - expect(invalid_gpg_signature.reload).to have_attributes( - project: project, - commit_sha: commit_sha, - gpg_key: nil, - gpg_key_primary_keyid: GpgHelpers::User1.fingerprint, - verification_status: 'unknown_key' - ) - end - it 'keeps the signature at being invalid when an unrelated gpg key is added' do # InvalidGpgSignatureUpdater is called by the after_create hook create :gpg_key, diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml index 4c71ae611bd..ae4dcd83f82 100644 --- a/spec/support/finder_collection_allowlist.yml +++ b/spec/support/finder_collection_allowlist.yml @@ -8,7 +8,8 @@ - Groups::EnvironmentScopesFinder # Reason: There is no need to have anything else besides the simple strucutre with the scope name - Security::RelatedPipelinesFinder # Reason: There is no need to have anything else besides the IDs of pipelines - Llm::ExtraResourceFinder # Reason: The finder does not deal with DB-backend resource for now. -- Security::VulnerabilityReadsElasticFinder # Reason: The finder deals with Elasticsearch records and not DB records +- Security::VulnerabilityElasticFinder # Reason: The finder deals with Elasticsearch records and not DB records +- Security::VulnerabilityElasticAggregationFinder # Reason: The finder deals with Elasticsearch records and not DB records # Temporary excludes (aka TODOs) # For example: