diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-14 12:08:53 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-14 12:08:53 +0000 |
commit | 8a5138ed7d38ccff8b5ca2fe0f7bbb77f8fdaad3 (patch) | |
tree | 4c0d373c990fc01cacff9b4093366ab398fcb7d3 | |
parent | 6d8f30ab0ae82678f10450d2158f24772f0c765c (diff) | |
download | gitlab-ce-8a5138ed7d38ccff8b5ca2fe0f7bbb77f8fdaad3.tar.gz |
Add latest changes from gitlab-org/gitlab@master
90 files changed, 1325 insertions, 527 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 8d10babc0ab..c826bd0a6c0 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -866,20 +866,27 @@ lib/gitlab/checks/** @proglottis @toon /doc/user/discussions/ @aqualls /doc/user/enterprise_user/ @jglassman1 /doc/user/feature_flags.md @sselhorn -/doc/user/group/ @lciutacu +/doc/user/group/access_and_permissions.md @lciutacu /doc/user/group/clusters/ @phillipwells /doc/user/group/compliance_frameworks.md @eread +/doc/user/group/contribution_analytics/ @lciutacu /doc/user/group/custom_project_templates.md @eread +/doc/user/group/devops_adoption/ @lciutacu /doc/user/group/epics/ @msedlakjakubowski /doc/user/group/import/ @eread +/doc/user/group/index.md @lciutacu +/doc/user/group/insights/ @lciutacu /doc/user/group/issues_analytics/ @msedlakjakubowski /doc/user/group/iterations/ @msedlakjakubowski +/doc/user/group/manage.md @lciutacu /doc/user/group/planning_hierarchy/ @msedlakjakubowski /doc/user/group/reporting/ @phillipwells /doc/user/group/repositories_analytics/ @drcatherinepope /doc/user/group/roadmap/ @msedlakjakubowski /doc/user/group/saml_sso/ @jglassman1 /doc/user/group/settings/ @jglassman1 +/doc/user/group/subgroups/ @lciutacu +/doc/user/group/value_stream_analytics/ @lciutacu /doc/user/infrastructure/ @phillipwells /doc/user/infrastructure/clusters/manage/management_project_applications/runner.md @fneill /doc/user/markdown.md @msedlakjakubowski @@ -891,6 +898,7 @@ lib/gitlab/checks/** @proglottis @toon /doc/user/permissions.md @jglassman1 /doc/user/product_analytics/ @lciutacu /doc/user/profile/account/ @jglassman1 +/doc/user/profile/achievements.md @lciutacu /doc/user/profile/comment_templates.md @aqualls /doc/user/profile/contributions_calendar.md @lciutacu /doc/user/profile/index.md @jglassman1 diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 3c9742f59e6..1b2f6b49558 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -63,6 +63,7 @@ stages: QA_INTERCEPT_REQUESTS: "true" GITLAB_LICENSE_MODE: test GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN + GITLAB_QA_OPTS: $EXTRA_GITLAB_QA_OPTS # todo: remove in 16.1 milestone when not needed for backwards compatibility anymore EE_LICENSE: $QA_EE_LICENSE GITHUB_ACCESS_TOKEN: $QA_GITHUB_ACCESS_TOKEN @@ -233,28 +234,6 @@ _quarantine: variables: QA_RSPEC_TAGS: --tag quarantine -# Temporary test job to support the effort of migrating to Super Sidebar -# https://gitlab.com/groups/gitlab-org/-/epics/9044 -_super-sidebar-nav: - extends: - - .qa - - .parallel - variables: - QA_SCENARIO: Test::Instance::Image - QA_KNAPSACK_REPORT_NAME: ee-instance - QA_TESTS: "" - QA_SUPER_SIDEBAR_ENABLED: "true" - QA_ALLURE_RESULTS_DIRECTORY: tmp/allure-results-super-sidebar - QA_EXPORT_TEST_METRICS: "false" - QA_DISABLE_RSPEC_RETRY: "true" - GITLAB_QA_OPTS: --set-feature-flags super_sidebar_nav=enabled - RSPEC_REPORT_OPTS: "--format documentation" - SKIP_REPORT_IN_ISSUES: "true" - allow_failure: true - rules: - - if: $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - - !reference [.rules:test:manual, rules] - # ------------------------------------------ # FF changes # ------------------------------------------ @@ -325,7 +304,7 @@ decomposition-single-db-selective: extends: .qa variables: QA_SCENARIO: Test::Instance::Image - GITLAB_QA_OPTS: --omnibus-config decomposition_single_db + GITLAB_QA_OPTS: --omnibus-config decomposition_single_db $EXTRA_GITLAB_QA_OPTS rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -342,7 +321,7 @@ decomposition-multiple-db-selective: variables: QA_SCENARIO: Test::Instance::Image GITLAB_ALLOW_SEPARATE_CI_DATABASE: "true" - GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db + GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db $EXTRA_GITLAB_QA_OPTS rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -359,7 +338,7 @@ object-storage-selective: variables: QA_SCENARIO: Test::Instance::Image QA_RSPEC_TAGS: --tag object_storage - GITLAB_QA_OPTS: --omnibus-config object_storage + GITLAB_QA_OPTS: --omnibus-config object_storage $EXTRA_GITLAB_QA_OPTS rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::ObjectStorage/ @@ -377,7 +356,7 @@ object-storage-aws-selective: AWS_S3_BUCKET_NAME: $QA_AWS_S3_BUCKET_NAME AWS_S3_KEY_ID: $QA_AWS_S3_KEY_ID AWS_S3_REGION: $QA_AWS_S3_REGION - GITLAB_QA_OPTS: --omnibus-config object_storage_aws + GITLAB_QA_OPTS: --omnibus-config object_storage_aws $EXTRA_GITLAB_QA_OPTS object-storage-aws: extends: object-storage-aws-selective parallel: 2 @@ -391,7 +370,7 @@ object-storage-gcs-selective: GOOGLE_PROJECT: $QA_GOOGLE_PROJECT GOOGLE_JSON_KEY: $QA_GOOGLE_JSON_KEY GOOGLE_CLIENT_EMAIL: $QA_GOOGLE_CLIENT_EMAIL - GITLAB_QA_OPTS: --omnibus-config object_storage_gcs + GITLAB_QA_OPTS: --omnibus-config object_storage_gcs $EXTRA_GITLAB_QA_OPTS object-storage-gcs: extends: object-storage-gcs-selective parallel: 2 @@ -403,7 +382,7 @@ packages-selective: variables: QA_SCENARIO: Test::Instance::Image QA_RSPEC_TAGS: --tag packages - GITLAB_QA_OPTS: --omnibus-config packages + GITLAB_QA_OPTS: --omnibus-config packages $EXTRA_GITLAB_QA_OPTS rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::Packages/ @@ -650,7 +629,7 @@ registry-object-storage-tls: QA_SCENARIO: Test::Integration::RegistryTLS QA_RSPEC_TAGS: "" GITLAB_TLS_CERTIFICATE: $QA_GITLAB_TLS_CERTIFICATE - GITLAB_QA_OPTS: --omnibus-config registry_object_storage + GITLAB_QA_OPTS: --omnibus-config registry_object_storage $EXTRA_GITLAB_QA_OPTS importers: extends: .qa @@ -671,27 +650,10 @@ e2e-test-report: - .rules:report:allure-report stage: report variables: - ALLURE_JOB_NAME: e2e-package-and-test GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE ALLURE_PROJECT_PATH: $CI_PROJECT_PATH ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID -# Temporary separate test report for super-sidebar test job -# TODO: remove once super-sidebar is on by default and enabled in tests -# https://gitlab.com/groups/gitlab-org/-/epics/9044 -e2e-test-report-super-sidebar: - extends: - - .generate-allure-report-base - stage: report - needs: - - _super-sidebar-nav - variables: - ALLURE_JOB_NAME: e2e-super-sidebar - ALLURE_RESULTS_GLOB: gitlab-qa-run-*/**/allure-results-super-sidebar - rules: - - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH - - !reference [.rules:test:manual, rules] - upload-knapsack-report: extends: - .generate-knapsack-report-base diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 2956eef6a9e..cf45c53b226 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -68,6 +68,7 @@ e2e:package-and-test-ee: RELEASE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/build/omnibus-gitlab-mirror/gitlab-ee:${CI_COMMIT_SHA}" GITLAB_QA_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-ee-qa:${CI_COMMIT_SHA}" RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler + ALLURE_JOB_NAME: e2e-package-and-test QA_PATH: qa # sets the optional path for bundler to run from QA_RUN_TYPE: e2e-package-and-test PIPELINE_NAME: E2E Omnibus GitLab EE @@ -106,6 +107,20 @@ e2e:package-and-test-ce: GITLAB_QA_IMAGE: ${CI_REGISTRY_IMAGE}/gitlab-ce-qa:${CI_COMMIT_SHA} PIPELINE_NAME: E2E Omnibus GitLab CE +e2e:package-and-test-super-sidebar: + extends: e2e:package-and-test-ee + variables: + QA_SUPER_SIDEBAR_ENABLED: "true" + QA_RUN_TYPE: e2e-package-and-test-super-sidebar + EXTRA_GITLAB_QA_OPTS: --set-feature-flags super_sidebar_nav=enabled + ALLURE_JOB_NAME: e2e-package-and-test-super-sidebar + PIPELINE_NAME: E2E Omnibus Super Sidebar + rules: + - if: $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH + allow_failure: true + - when: manual + allow_failure: true + e2e:test-on-gdk: extends: - .qa:rules:e2e:test-on-gdk diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index bb9ea30f0d4..704c857760d 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -860,14 +860,27 @@ - <<: *if-not-canonical-namespace when: never - <<: *if-merge-request-targeting-stable-branch + - <<: *if-ruby2-branch - <<: *if-merge-request-labels-run-review-app - <<: *if-merge-request-labels-run-all-e2e - <<: *if-auto-deploy-branches - - <<: *if-ruby2-branch - <<: *if-default-refs changes: *ci-build-images-patterns - <<: *if-default-refs changes: *code-qa-patterns + # Rules to support .qa:rules:package-and-test-mrs + - <<: *if-merge-request + changes: *dependency-patterns + - <<: *if-merge-request-labels-run-all-e2e + - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e + changes: *feature-flag-development-config-patterns + - <<: *if-merge-request + changes: *feature-flag-development-config-patterns + - <<: *if-merge-request + changes: *nodejs-patterns + - <<: *if-merge-request + changes: *ci-qa-patterns + - <<: *if-force-ci .build-images:rules:build-assets-image-as-if-foss: rules: @@ -1305,7 +1318,7 @@ allow_failure: true - <<: *if-ruby2-branch allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *dependency-patterns allow_failure: true variables: @@ -1316,30 +1329,30 @@ changes: *feature-flag-development-config-patterns when: manual allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *feature-flag-development-config-patterns allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *initializers-patterns allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *nodejs-patterns allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *ci-qa-patterns allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: - qa/Gemfile.lock # qa/Gemfile.lock is a part of *qa-patterns, so this rule must be placed before the one with *qa-patterns changes variables: UPDATE_QA_CACHE: "true" - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *qa-patterns allow_failure: true - <<: *if-dot-com-gitlab-org-and-security-merge-request-and-qa-tests-specified changes: *code-patterns allow_failure: true - - <<: *if-dot-com-gitlab-org-and-security-merge-request + - <<: *if-merge-request changes: *code-patterns when: manual allow_failure: true diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml index 88e0d7c7a85..fe8a1cb6bfa 100644 --- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml +++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml @@ -389,7 +389,6 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/presenters/packages/pypi/simple_index_presenter_spec.rb' - 'spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb' - 'spec/presenters/pages_domain_presenter_spec.rb' - - 'spec/presenters/project_clusterable_presenter_spec.rb' - 'spec/presenters/project_hook_presenter_spec.rb' - 'spec/presenters/project_presenter_spec.rb' - 'spec/presenters/projects/import_export/project_export_presenter_spec.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index c4b8c206433..a0e239e05b5 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -5524,7 +5524,6 @@ RSpec/MissingFeatureCategory: - 'spec/presenters/packages/pypi/simple_index_presenter_spec.rb' - 'spec/presenters/packages/pypi/simple_package_versions_presenter_spec.rb' - 'spec/presenters/pages_domain_presenter_spec.rb' - - 'spec/presenters/project_clusterable_presenter_spec.rb' - 'spec/presenters/project_hook_presenter_spec.rb' - 'spec/presenters/project_member_presenter_spec.rb' - 'spec/presenters/project_presenter_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index 098cf10c7fc..f9e4d1b735b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 15.10.3 (2023-04-14) + +### Fixed (3 changes) + +- [Backport fixes for broadcast messages](gitlab-org/gitlab@c97c17e31e99f9e93127245cd1f65f7d15cdb0ef) ([merge request](gitlab-org/gitlab!117276)) +- [Fix automatically-retried jobs stuck in pending state](gitlab-org/gitlab@e349581eaf1e050b8bcdee76f9d40f0c182a09f8) ([merge request](gitlab-org/gitlab!117280)) +- [Verify deploy keys settings for protected tags (backport)](gitlab-org/gitlab@4bd6914bd616c1d8dc9ee7cb75e92be13d522ca9) ([merge request](gitlab-org/gitlab!116952)) + +### Changed (1 change) + +- [Change the order of vulnerability creation](gitlab-org/gitlab@4193c4cab75f9472b3804b74b17f4a10f3ae9580) ([merge request](gitlab-org/gitlab!116851)) **GitLab Enterprise Edition** + ## 15.10.2 (2023-04-05) ### Fixed (3 changes) @@ -249,6 +249,7 @@ gem 're2', '~> 1.6.0' # Misc +gem 'semver_dialects', '~> 1.2.1' gem 'version_sorter', '~> 2.3' # Export Ruby Regex to Javascript diff --git a/Gemfile.checksum b/Gemfile.checksum index 4a0022198f4..40470b66dac 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -556,6 +556,7 @@ {"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"}, {"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"}, {"name":"selenium-webdriver","version":"3.142.7","platform":"ruby","checksum":"dea0993e0e4fdb364f0453144814c0e6099a411d17396807c6cac666d0ddac29"}, +{"name":"semver_dialects","version":"1.2.1","platform":"ruby","checksum":"60a1f67659f79c51a667e8858ec9b089c1e4ce4f6d2a0f0b4ac101916946eb23"}, {"name":"sentry-rails","version":"5.8.0","platform":"ruby","checksum":"c11b2d909de2c2bfda793c45f64180fd784d54c46886338b683ee3f8efa7731b"}, {"name":"sentry-raven","version":"3.1.2","platform":"ruby","checksum":"103d3b122958810d34898ce2e705bcf549ddb9d855a70ce9a3970ee2484f364a"}, {"name":"sentry-ruby","version":"5.8.0","platform":"ruby","checksum":"caeb121433be379fb94e991a45265a287b13a9a9083e7264f539752369d37110"}, @@ -635,6 +636,7 @@ {"name":"train-core","version":"3.4.9","platform":"ruby","checksum":"d7ad8fa9a379c43a30baaaf1141af1cb28349d386c054f7fc81d169a625d6edd"}, {"name":"truncato","version":"0.7.12","platform":"ruby","checksum":"fed9e8a04fa35fd1a64506cd2089761bae4adfe47e756c3ce98a5c43856c9c4c"}, {"name":"tty-color","version":"0.6.0","platform":"ruby","checksum":"6f9c37ca3a4e2367fb2e6d09722762647d6f455c111f05b59f35730eeb24332a"}, +{"name":"tty-command","version":"0.10.1","platform":"ruby","checksum":"0c6c471fcb932d55518734eb4e2e07e9efdd2918713cc39bb7393ba862471192"}, {"name":"tty-cursor","version":"0.7.1","platform":"ruby","checksum":"79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48"}, {"name":"tty-markdown","version":"0.7.2","platform":"ruby","checksum":"1ed81db97028d006ba81e2cfd9fe0a04b0eb28650ad0d4086ed6e5627f4ac511"}, {"name":"tty-prompt","version":"0.23.1","platform":"ruby","checksum":"fcdbce905238993f27eecfdf67597a636bc839d92192f6a0eef22b8166449ec8"}, diff --git a/Gemfile.lock b/Gemfile.lock index dbd315129ec..73480837892 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1403,6 +1403,10 @@ GEM selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) + semver_dialects (1.2.1) + pastel (~> 0.8.0) + thor (~> 1.2.0) + tty-command (~> 0.10.1) sentry-rails (5.8.0) railties (>= 5.0) sentry-ruby (~> 5.8.0) @@ -1548,6 +1552,8 @@ GEM htmlentities (~> 4.3.1) nokogiri (>= 1.7.0, <= 2.0) tty-color (0.6.0) + tty-command (0.10.1) + pastel (~> 0.8) tty-cursor (0.7.1) tty-markdown (0.7.2) kramdown (>= 1.16.2, < 3.0) @@ -1908,6 +1914,7 @@ DEPENDENCIES sd_notify (~> 0.1.0) seed-fu (~> 2.3.7) selenium-webdriver (~> 3.142, >= 3.142.7) + semver_dialects (~> 1.2.1) sentry-rails (~> 5.8.0) sentry-raven (~> 3.1) sentry-ruby (~> 5.8.0) diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 0f1c13f818f..c3a4897ce78 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -376,7 +376,7 @@ export default { v-if="isReviewable && showLocalFileReviews" v-gl-tooltip.hover data-testid="fileReviewCheckbox" - class="gl-mr-5 gl-display-flex gl-align-items-center" + class="gl-mr-5 gl-mb-n3 gl-display-flex gl-align-items-center" :title="$options.i18n.fileReviewTooltip" :checked="reviewed" @change="toggleReview" diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue index c89e843b660..b4751d51fcb 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue @@ -1,4 +1,5 @@ <script> +import { v4 as uuidv4 } from 'uuid'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { CHART_CONTAINER_HEIGHT } from './constants'; @@ -17,6 +18,15 @@ export default { required: true, }, }, + data: () => ({ + chartKey: uuidv4(), + }), + watch: { + chartData() { + // Re-render area chart when the data changes + this.chartKey = uuidv4(); + }, + }, chartContainerHeight: CHART_CONTAINER_HEIGHT, }; </script> @@ -27,6 +37,7 @@ export default { </p> <gl-area-chart v-bind="$attrs" + :key="chartKey" responsive width="auto" :height="$options.chartContainerHeight" diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue index 47b96934420..e5d82bad54f 100644 --- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue +++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue @@ -39,11 +39,10 @@ export default { </script> <template> <div> - <segmented-control-button-group - v-model="selectedChart" - :options="chartRanges" - class="gl-mb-4" - /> + <div class="gl-display-flex gl-flex-wrap-wrap gl-gap-5"> + <segmented-control-button-group v-model="selectedChart" :options="chartRanges" /> + <slot name="extend-button-group"></slot> + </div> <ci-cd-analytics-area-chart v-if="chart" v-bind="$attrs" diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 8f482cf6e2f..bc6e67a3a7d 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -169,18 +169,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController gon.push(webauthn: { options: options, app_id: u2f_app_id }) end - # Adds delete path to u2f registrations - # to reduce logic in view template - def u2f_registrations - current_user.u2f_registrations.map do |u2f_registration| - { - name: u2f_registration.name, - created_at: u2f_registration.created_at, - delete_path: profile_u2f_registration_path(u2f_registration) - } - end - end - def webauthn_registrations current_user.webauthn_registrations.map do |webauthn_registration| { @@ -235,10 +223,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController @qr_code = build_qr_code @account_string = account_string - if Feature.enabled?(:webauthn) - setup_webauthn_registration - else - setup_u2f_registration - end + setup_webauthn_registration end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 44c8c8f8f44..da15b393e6c 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -10,9 +10,6 @@ class ProfilesController < Profiles::ApplicationController check_rate_limit!(:profile_update_username, scope: current_user) end skip_before_action :require_email, only: [:show, :update] - before_action do - push_frontend_feature_flag(:webauthn) - end feature_category :user_profile, [:show, :update, :reset_incoming_email_token, :reset_feed_token, :reset_static_object_token, :update_username] diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index bd5701a3557..cc0d3818e33 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -20,28 +20,14 @@ class Projects::BlameController < Projects::ApplicationController end load_environment - - @blame_mode = Gitlab::Git::BlameMode.new(@commit.project, blame_params) - blame_service = Projects::BlameService.new(@blob, @commit, @blame_mode, blame_params) - - @blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate! - - @blame_pagination = blame_service.pagination - - @blame_per_page = blame_service.per_page - - render locals: { total_extra_pages: blame_service.total_extra_pages } + load_blame end def page @blob = @repository.blob_at(@commit.id, @path) load_environment - - @blame_mode = Gitlab::Git::BlameMode.new(@commit.project, blame_params) - blame_service = Projects::BlameService.new(@blob, @commit, @blame_mode, blame_params) - - @blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate! + load_blame render partial: 'page' end @@ -54,6 +40,14 @@ class Projects::BlameController < Projects::ApplicationController @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last end + def load_blame + @blame_mode = Gitlab::Git::BlameMode.new(@commit.project, blame_params) + @blame_pagination = Gitlab::Git::BlamePagination.new(@blob, @blame_mode, blame_params) + + blame = Gitlab::Blame.new(@blob, @commit, range: @blame_pagination.blame_range) + @blame = Gitlab::View::Presenter::Factory.new(blame, project: @project, path: @path, page: @blame_pagination.page).fabricate! + end + def blame_params params.permit(:page, :no_pagination, :streaming) end diff --git a/app/models/clusters/agents/authorizations/user_access/group_authorization.rb b/app/models/clusters/agents/authorizations/user_access/group_authorization.rb new file mode 100644 index 00000000000..088ce1c0e27 --- /dev/null +++ b/app/models/clusters/agents/authorizations/user_access/group_authorization.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module UserAccess + class GroupAuthorization < ApplicationRecord + self.table_name = 'agent_user_access_group_authorizations' + + belongs_to :agent, class_name: 'Clusters::Agent', optional: false + belongs_to :group, class_name: '::Group', optional: false + + validates :config, json_schema: { filename: 'clusters_agents_authorizations_user_access_config' } + + def config_project + agent.project + end + end + end + end + end +end diff --git a/app/models/clusters/agents/authorizations/user_access/project_authorization.rb b/app/models/clusters/agents/authorizations/user_access/project_authorization.rb new file mode 100644 index 00000000000..22b2c3fea22 --- /dev/null +++ b/app/models/clusters/agents/authorizations/user_access/project_authorization.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module UserAccess + class ProjectAuthorization < ApplicationRecord + self.table_name = 'agent_user_access_project_authorizations' + + belongs_to :agent, class_name: 'Clusters::Agent', optional: false + belongs_to :project, class_name: '::Project', optional: false + + validates :config, json_schema: { filename: 'clusters_agents_authorizations_user_access_config' } + + def config_project + agent.project + end + end + end + end + end +end diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb index f6617fa0888..1fef155e6ea 100644 --- a/app/models/members_preloader.rb +++ b/app/models/members_preloader.rb @@ -8,15 +8,12 @@ class MembersPreloader end def preload_all - user_associations = [:status] - user_associations << :webauthn_registrations if Feature.enabled?(:webauthn) - ActiveRecord::Associations::Preloader.new( records: members, associations: [ :source, :created_by, - { user: user_associations } + { user: [:status, :webauthn_registrations] } ] ).call end diff --git a/app/models/user.rb b/app/models/user.rb index 738e1eba982..16b4e604a9e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1087,8 +1087,6 @@ class User < ApplicationRecord end def two_factor_webauthn_enabled? - return false unless Feature.enabled?(:webauthn) - (webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?) end diff --git a/app/services/projects/blame_service.rb b/app/services/projects/blame_service.rb deleted file mode 100644 index d4c01044828..00000000000 --- a/app/services/projects/blame_service.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -# Service class to correctly initialize Gitlab::Blame and Kaminari pagination -# objects -module Projects - class BlameService - PER_PAGE = 1000 - STREAMING_FIRST_PAGE_SIZE = 200 - STREAMING_PER_PAGE = 2000 - - def initialize(blob, commit, blame_mode, params) - @blob = blob - @commit = commit - @blame_mode = blame_mode - @page = extract_page(params) - @params = params - end - - attr_reader :page - - def blame - Gitlab::Blame.new(blob, commit, range: blame_range) - end - - def pagination - return unless blame_mode.pagination? - - Kaminari.paginate_array([], total_count: blob_lines_count, limit: per_page) - .tap { |pagination| pagination.max_paginates_per(per_page) } - .page(page) - end - - def per_page - blame_mode.streaming? ? STREAMING_PER_PAGE : PER_PAGE - end - - def total_pages - total = (blob_lines_count.to_f / per_page).ceil - return total unless blame_mode.streaming? - - ([blob_lines_count - STREAMING_FIRST_PAGE_SIZE, 0].max.to_f / per_page).ceil + 1 - end - - def total_extra_pages - [total_pages - 1, 0].max - end - - private - - attr_reader :blob, :commit, :blame_mode - - def blame_range - return if blame_mode.full? - - first_line = (page - 1) * per_page + 1 - - if blame_mode.streaming? - return 1..STREAMING_FIRST_PAGE_SIZE if page == 1 - - first_line = STREAMING_FIRST_PAGE_SIZE + (page - 2) * per_page + 1 - end - - last_line = (first_line + per_page).to_i - 1 - - first_line..last_line - end - - def extract_page(params) - page = params.fetch(:page, 1).to_i - - return 1 if page < 1 || overlimit?(page) - - page - end - - def overlimit?(page) - page > total_pages - end - - def blob_lines_count - @blob_lines_count ||= blob.data.lines.count - end - end -end diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb index 353456c545d..53ec37d0ff7 100644 --- a/app/services/users/approve_service.rb +++ b/app/services/users/approve_service.rb @@ -17,6 +17,11 @@ module Users user.accept_pending_invitations! if user.active_for_authentication? DeviseMailer.user_admin_approval(user).deliver_later + if user.created_by_id + reset_token = user.generate_reset_token + NotificationService.new.new_user(user, reset_token) + end + log_event(user) after_approve_hook(user) success(message: 'Success', http_status: :created) diff --git a/app/validators/json_schemas/clusters_agents_authorizations_user_access_config.json b/app/validators/json_schemas/clusters_agents_authorizations_user_access_config.json new file mode 100644 index 00000000000..75624af9e6a --- /dev/null +++ b/app/validators/json_schemas/clusters_agents_authorizations_user_access_config.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Cluster Agent configuration for an authorized project or group through user_access keyword", + "type": "object", + "additionalProperties": true +} diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 5e62d6c5421..161e7aef253 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -6,7 +6,7 @@ - if readme_path = @project.repository.readme_path - add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json") -#tree-holder.tree-holder.clearfix.js-per-page{ data: { blame_per_page: Projects::BlameService::PER_PAGE } } +#tree-holder.tree-holder.clearfix.js-per-page{ data: { blame_per_page: Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE } } .info-well.gl-display-none.gl-sm-display-flex.project-last-commit.gl-flex-direction-column.gl-mt-5 #js-last-commit.gl-m-auto = gl_loading_icon(size: 'md') diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 689dcb3b2cb..a56d398d3a0 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -2,7 +2,7 @@ - add_page_specific_style 'page_bundles/tree' - blame_streaming_url = blame_pages_streaming_url(@id, @project) -- if @blame_mode.streaming? && total_extra_pages > 0 +- if @blame_mode.streaming? && @blame_pagination.total_extra_pages > 0 - content_for :startup_js do = javascript_tag do :plain @@ -11,7 +11,7 @@ url.searchParams.set('page', 2); return fetch(url).then(response => response.body); })(); -- dataset = { testid: 'blob-content-holder', qa_selector: 'blame_file_content', per_page: @blame_per_page, total_extra_pages: total_extra_pages - 1, pages_url: blame_streaming_url } +- dataset = { testid: 'blob-content-holder', qa_selector: 'blame_file_content', per_page: @blame_pagination.per_page, total_extra_pages: @blame_pagination.total_extra_pages - 1, pages_url: blame_streaming_url } #blob-content-holder.tree-holder.js-per-page{ data: dataset } = render "projects/blob/breadcrumb", blob: @blob, blame: true @@ -53,5 +53,4 @@ = _('Loading full blame...') - if @blame_mode.pagination? - = paginate(@blame_pagination, theme: "gitlab") - + = paginate(@blame_pagination.paginator, theme: "gitlab") diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index d11bf36a610..1a751849b3a 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -16,7 +16,7 @@ - if project.forked? #js-fork-info{ data: vue_fork_divergence_data(project, ref) } -#blob-content-holder.blob-content-holder.js-per-page{ data: { blame_per_page: Projects::BlameService::PER_PAGE } } +#blob-content-holder.blob-content-holder.js-per-page{ data: { blame_per_page: Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE } } - if @code_navigation_path #js-code-navigation{ data: { code_navigation_path: @code_navigation_path, blob_path: blob.path, definition_path_prefix: project_blob_path(@project, @ref) } } - if !expanded diff --git a/config/feature_flags/development/ci_multi_doc_yaml.yml b/config/feature_flags/development/ci_multi_doc_yaml.yml index 4e6289abefa..ec86ecd95c2 100644 --- a/config/feature_flags/development/ci_multi_doc_yaml.yml +++ b/config/feature_flags/development/ci_multi_doc_yaml.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388836 milestone: '15.9' type: development group: group::pipeline authoring -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/webauthn.yml b/config/feature_flags/development/webauthn.yml deleted file mode 100644 index 6bd4fc95020..00000000000 --- a/config/feature_flags/development/webauthn.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: webauthn -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232671 -milestone: '13.4' -type: development -group: group::authentication and authorization -default_enabled: true diff --git a/data/deprecations/15-1-jira-github-enterprise-dvcs.yml b/data/deprecations/15-1-jira-github-enterprise-dvcs.yml index 8c69dce35fe..34552360141 100644 --- a/data/deprecations/15-1-jira-github-enterprise-dvcs.yml +++ b/data/deprecations/15-1-jira-github-enterprise-dvcs.yml @@ -1,10 +1,11 @@ -- title: "Jira GitHub Enterprise DVCS integration" # The name of the feature to be deprecated +- title: "Jira DVCS connector for Jira Cloud" # The name of the feature to be deprecated announcement_milestone: "15.1" # The milestone when this feature was first announced as deprecated. removal_milestone: "16.0" # The milestone when this feature is planned to be removed breaking_change: true # If this deprecation is a breaking change, set this value to true body: | # Do not modify this line, instead modify the lines below. - The [Jira DVCS Connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) (which enables the [Jira Development Panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/)), will no longer support Jira Cloud users starting with GitLab 16.0. The [GitLab for Jira App](https://docs.gitlab.com/ee/integration/jira/connect-app.html) has always been recommended for Jira Cloud users, and it will be required instead of the DVCS connector. If you are a Jira Cloud user, we recommended you begin migrating to the GitLab for Jira App. - Any Jira Server and Jira Data Center users will need to confirm they are not using the GitHub Enterprise Connector to enable the GitLab DVCS integration, but they may continue to use the [native GitLab DVCS integration](https://docs.gitlab.com/ee/integration/jira/dvcs/) (supported in Jira 8.14 and later). + The [Jira DVCS connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) for Jira Cloud has been deprecated and will be removed in GitLab 16.0. If you're using the Jira DVCS connector with Jira Cloud, migrate to the [GitLab for Jira Cloud app](https://docs.gitlab.com/ee/integration/jira/connect-app.html). + + The Jira DVCS connector is also deprecated for Jira 8.13 and earlier. You can only use the Jira DVCS connector with Jira Server or Jira Data Center in Jira 8.14 and later. # The following items are not published on the docs page, but may be used in the future. stage: Manage # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] diff --git a/db/docs/agent_user_access_group_authorizations.yml b/db/docs/agent_user_access_group_authorizations.yml new file mode 100644 index 00000000000..659b36bd61f --- /dev/null +++ b/db/docs/agent_user_access_group_authorizations.yml @@ -0,0 +1,10 @@ +--- +table_name: agent_user_access_group_authorizations +classes: +- Clusters::Agents::Authorizations::UserAccess::GroupAuthorization +feature_categories: +- kubernetes_management +description: Configuration for a group that is authorized to use a particular cluster agent through user_access keyword +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116901 +milestone: '15.11' +gitlab_schema: gitlab_main diff --git a/db/docs/agent_user_access_project_authorizations.yml b/db/docs/agent_user_access_project_authorizations.yml new file mode 100644 index 00000000000..0f0953da630 --- /dev/null +++ b/db/docs/agent_user_access_project_authorizations.yml @@ -0,0 +1,10 @@ +--- +table_name: agent_user_access_project_authorizations +classes: +- Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization +feature_categories: +- kubernetes_management +description: Configuration for a project that is authorized to use a particular cluster agent through user_access keyword +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116901 +milestone: '15.11' +gitlab_schema: gitlab_main diff --git a/db/docs/batched_background_migrations/backfill_admin_mode_scope_for_personal_access_tokens.yml b/db/docs/batched_background_migrations/backfill_admin_mode_scope_for_personal_access_tokens.yml new file mode 100644 index 00000000000..33f3371e294 --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_admin_mode_scope_for_personal_access_tokens.yml @@ -0,0 +1,6 @@ +--- +migration_job_name: BackfillAdminModeScopeForPersonalAccessTokens +description: backfills `admin_mode` scope to personal access tokens associated to administrators +feature_category: system_access +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107875 +milestone: 15.8 diff --git a/db/migrate/20230406150254_create_agent_user_access_project_authorizations_table.rb b/db/migrate/20230406150254_create_agent_user_access_project_authorizations_table.rb new file mode 100644 index 00000000000..1adc3bb001a --- /dev/null +++ b/db/migrate/20230406150254_create_agent_user_access_project_authorizations_table.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateAgentUserAccessProjectAuthorizationsTable < Gitlab::Database::Migration[2.1] + INDEX_NAME_1 = 'index_agent_user_access_on_project_id' + INDEX_NAME_2 = 'index_agent_user_access_on_agent_id_and_project_id' + + def change + create_table :agent_user_access_project_authorizations do |t| + t.bigint :project_id, null: false + t.bigint :agent_id, null: false + t.jsonb :config, null: false + + t.index [:project_id], name: INDEX_NAME_1 + t.index [:agent_id, :project_id], unique: true, name: INDEX_NAME_2 + end + end +end diff --git a/db/migrate/20230406150354_create_agent_user_access_group_authorizations_table.rb b/db/migrate/20230406150354_create_agent_user_access_group_authorizations_table.rb new file mode 100644 index 00000000000..1d4df7d7330 --- /dev/null +++ b/db/migrate/20230406150354_create_agent_user_access_group_authorizations_table.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateAgentUserAccessGroupAuthorizationsTable < Gitlab::Database::Migration[2.1] + INDEX_NAME_1 = 'index_agent_user_access_on_group_id' + INDEX_NAME_2 = 'index_agent_user_access_on_agent_id_and_group_id' + + def change + create_table :agent_user_access_group_authorizations do |t| + t.bigint :group_id, null: false + t.bigint :agent_id, null: false + t.jsonb :config, null: false + + t.index [:group_id], name: INDEX_NAME_1 + t.index [:agent_id, :group_id], unique: true, name: INDEX_NAME_2 + end + end +end diff --git a/db/migrate/20230406150454_add_fks_to_agent_user_access_authorizations.rb b/db/migrate/20230406150454_add_fks_to_agent_user_access_authorizations.rb new file mode 100644 index 00000000000..62f00620108 --- /dev/null +++ b/db/migrate/20230406150454_add_fks_to_agent_user_access_authorizations.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class AddFksToAgentUserAccessAuthorizations < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :agent_user_access_project_authorizations, :projects, + column: :project_id, on_delete: :cascade + add_concurrent_foreign_key :agent_user_access_project_authorizations, :cluster_agents, + column: :agent_id, on_delete: :cascade + add_concurrent_foreign_key :agent_user_access_group_authorizations, :namespaces, + column: :group_id, on_delete: :cascade + add_concurrent_foreign_key :agent_user_access_group_authorizations, :cluster_agents, + column: :agent_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :agent_user_access_project_authorizations, column: :project_id + end + + with_lock_retries do + remove_foreign_key_if_exists :agent_user_access_project_authorizations, column: :agent_id + end + + with_lock_retries do + remove_foreign_key_if_exists :agent_user_access_group_authorizations, column: :group_id + end + + with_lock_retries do + remove_foreign_key_if_exists :agent_user_access_group_authorizations, column: :agent_id + end + end +end diff --git a/db/post_migrate/20221228103133_queue_backfill_admin_mode_scope_for_personal_access_tokens.rb b/db/post_migrate/20221228103133_queue_backfill_admin_mode_scope_for_personal_access_tokens.rb index c111d5090e1..577d55f4df6 100644 --- a/db/post_migrate/20221228103133_queue_backfill_admin_mode_scope_for_personal_access_tokens.rb +++ b/db/post_migrate/20221228103133_queue_backfill_admin_mode_scope_for_personal_access_tokens.rb @@ -1,21 +1,11 @@ # frozen_string_literal: true class QueueBackfillAdminModeScopeForPersonalAccessTokens < Gitlab::Database::Migration[2.1] - MIGRATION = 'BackfillAdminModeScopeForPersonalAccessTokens' - DELAY_INTERVAL = 2.minutes - restrict_gitlab_migration gitlab_schema: :gitlab_main - def up - queue_batched_background_migration( - MIGRATION, - :personal_access_tokens, - :id, - job_interval: DELAY_INTERVAL - ) - end + # no-op as the original migration is rescheduled + # in migrations version 20230406093640 + def up; end - def down - delete_batched_background_migration(MIGRATION, :personal_access_tokens, :id, []) - end + def down; end end diff --git a/db/post_migrate/20230406093640_requeue_backfill_admin_mode_scope_for_personal_access_tokens.rb b/db/post_migrate/20230406093640_requeue_backfill_admin_mode_scope_for_personal_access_tokens.rb new file mode 100644 index 00000000000..17ba9edef22 --- /dev/null +++ b/db/post_migrate/20230406093640_requeue_backfill_admin_mode_scope_for_personal_access_tokens.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class RequeueBackfillAdminModeScopeForPersonalAccessTokens < Gitlab::Database::Migration[2.1] + MIGRATION = 'BackfillAdminModeScopeForPersonalAccessTokens' + DELAY_INTERVAL = 2.minutes + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + delete_batched_background_migration(MIGRATION, :personal_access_tokens, :id, []) + + queue_batched_background_migration( + MIGRATION, + :personal_access_tokens, + :id, + job_interval: DELAY_INTERVAL + ) + end + + def down + delete_batched_background_migration(MIGRATION, :personal_access_tokens, :id, []) + end +end diff --git a/db/schema_migrations/20230406093640 b/db/schema_migrations/20230406093640 new file mode 100644 index 00000000000..3bc9003b2fa --- /dev/null +++ b/db/schema_migrations/20230406093640 @@ -0,0 +1 @@ +a49416e1b59ffb29bf2015c96e6bdf92428036862102fbbfa63284cc1da53c82
\ No newline at end of file diff --git a/db/schema_migrations/20230406150254 b/db/schema_migrations/20230406150254 new file mode 100644 index 00000000000..3e3463a76f9 --- /dev/null +++ b/db/schema_migrations/20230406150254 @@ -0,0 +1 @@ +2b8aea677f295a0ab8f5ca9fbe7162156a06de89bd30ab5b252eb4460bcc7a2e
\ No newline at end of file diff --git a/db/schema_migrations/20230406150354 b/db/schema_migrations/20230406150354 new file mode 100644 index 00000000000..484af1e53ad --- /dev/null +++ b/db/schema_migrations/20230406150354 @@ -0,0 +1 @@ +2f1ef88ab1731b20821a86a74006ed0856d3c7baa5e197f72410aedb15cb2894
\ No newline at end of file diff --git a/db/schema_migrations/20230406150454 b/db/schema_migrations/20230406150454 new file mode 100644 index 00000000000..f7237bd5ef2 --- /dev/null +++ b/db/schema_migrations/20230406150454 @@ -0,0 +1 @@ +9966f807ce21016777a87d437355241cd8e5cacf2ccd143258ef0446e6f26e93
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 26d2a9c5787..600cac16e96 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10858,6 +10858,38 @@ CREATE SEQUENCE agent_project_authorizations_id_seq ALTER SEQUENCE agent_project_authorizations_id_seq OWNED BY agent_project_authorizations.id; +CREATE TABLE agent_user_access_group_authorizations ( + id bigint NOT NULL, + group_id bigint NOT NULL, + agent_id bigint NOT NULL, + config jsonb NOT NULL +); + +CREATE SEQUENCE agent_user_access_group_authorizations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE agent_user_access_group_authorizations_id_seq OWNED BY agent_user_access_group_authorizations.id; + +CREATE TABLE agent_user_access_project_authorizations ( + id bigint NOT NULL, + project_id bigint NOT NULL, + agent_id bigint NOT NULL, + config jsonb NOT NULL +); + +CREATE SEQUENCE agent_user_access_project_authorizations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE agent_user_access_project_authorizations_id_seq OWNED BY agent_user_access_project_authorizations.id; + CREATE TABLE alert_management_alert_assignees ( id bigint NOT NULL, user_id bigint NOT NULL, @@ -24534,6 +24566,10 @@ ALTER TABLE ONLY agent_group_authorizations ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY agent_project_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_project_authorizations_id_seq'::regclass); +ALTER TABLE ONLY agent_user_access_group_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_user_access_group_authorizations_id_seq'::regclass); + +ALTER TABLE ONLY agent_user_access_project_authorizations ALTER COLUMN id SET DEFAULT nextval('agent_user_access_project_authorizations_id_seq'::regclass); + ALTER TABLE ONLY alert_management_alert_assignees ALTER COLUMN id SET DEFAULT nextval('alert_management_alert_assignees_id_seq'::regclass); ALTER TABLE ONLY alert_management_alert_metric_images ALTER COLUMN id SET DEFAULT nextval('alert_management_alert_metric_images_id_seq'::regclass); @@ -26234,6 +26270,12 @@ ALTER TABLE ONLY agent_group_authorizations ALTER TABLE ONLY agent_project_authorizations ADD CONSTRAINT agent_project_authorizations_pkey PRIMARY KEY (id); +ALTER TABLE ONLY agent_user_access_group_authorizations + ADD CONSTRAINT agent_user_access_group_authorizations_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY agent_user_access_project_authorizations + ADD CONSTRAINT agent_user_access_project_authorizations_pkey PRIMARY KEY (id); + ALTER TABLE ONLY alert_management_alert_assignees ADD CONSTRAINT alert_management_alert_assignees_pkey PRIMARY KEY (id); @@ -29409,6 +29451,14 @@ CREATE UNIQUE INDEX index_agent_project_authorizations_on_agent_id_and_project_i CREATE INDEX index_agent_project_authorizations_on_project_id ON agent_project_authorizations USING btree (project_id); +CREATE UNIQUE INDEX index_agent_user_access_on_agent_id_and_group_id ON agent_user_access_group_authorizations USING btree (agent_id, group_id); + +CREATE UNIQUE INDEX index_agent_user_access_on_agent_id_and_project_id ON agent_user_access_project_authorizations USING btree (agent_id, project_id); + +CREATE INDEX index_agent_user_access_on_group_id ON agent_user_access_group_authorizations USING btree (group_id); + +CREATE INDEX index_agent_user_access_on_project_id ON agent_user_access_project_authorizations USING btree (project_id); + CREATE INDEX index_alert_assignees_on_alert_id ON alert_management_alert_assignees USING btree (alert_id); CREATE UNIQUE INDEX index_alert_assignees_on_user_id_and_alert_id ON alert_management_alert_assignees USING btree (user_id, alert_id); @@ -34320,6 +34370,9 @@ ALTER TABLE ONLY epics ALTER TABLE ONLY environments ADD CONSTRAINT fk_01a033a308 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE SET NULL; +ALTER TABLE ONLY agent_user_access_project_authorizations + ADD CONSTRAINT fk_0250c0ad51 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY incident_management_escalation_rules ADD CONSTRAINT fk_0314ee86eb FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -34617,6 +34670,9 @@ ALTER TABLE ONLY alert_management_alerts ALTER TABLE ONLY path_locks ADD CONSTRAINT fk_5265c98f24 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY agent_user_access_group_authorizations + ADD CONSTRAINT fk_53fd98ccbf FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY clusters_applications_prometheus ADD CONSTRAINT fk_557e773639 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE; @@ -34737,6 +34793,9 @@ ALTER TABLE ONLY vulnerabilities ALTER TABLE ONLY oauth_openid_requests ADD CONSTRAINT fk_77114b3b09 FOREIGN KEY (access_grant_id) REFERENCES oauth_access_grants(id) ON DELETE CASCADE; +ALTER TABLE ONLY agent_user_access_project_authorizations + ADD CONSTRAINT fk_78034b05d8 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY users ADD CONSTRAINT fk_789cd90b35 FOREIGN KEY (accepted_term_id) REFERENCES application_setting_terms(id) ON DELETE CASCADE; @@ -34854,6 +34913,9 @@ ALTER TABLE ONLY boards_epic_list_user_preferences ALTER TABLE ONLY issues ADD CONSTRAINT fk_96b1dd429c FOREIGN KEY (milestone_id) REFERENCES milestones(id) ON DELETE SET NULL; +ALTER TABLE ONLY agent_user_access_group_authorizations + ADD CONSTRAINT fk_97ce8e8284 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE; + ALTER TABLE ONLY vulnerability_occurrences ADD CONSTRAINT fk_97ffe77653 FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE SET NULL; diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md index 359f53c5a2e..afc2f74201a 100644 --- a/doc/administration/auth/oidc.md +++ b/doc/administration/auth/oidc.md @@ -40,7 +40,7 @@ The OpenID Connect provides you with a client's details and secret for you to us ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Provider name", # optional label for login button, defaults to "Openid Connect" icon: "<custom_provider_icon>", args: { @@ -66,7 +66,7 @@ The OpenID Connect provides you with a client's details and secret for you to us For installation from source: ```yaml - - { name: 'openid_connect', + - { name: 'openid_connect', # do not change this parameter label: 'Provider name', # optional label for login button, defaults to "Openid Connect" icon: '<custom_provider_icon>', args: { @@ -165,7 +165,7 @@ for more details: ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Google OpenID", # optional label for login button, defaults to "Openid Connect" args: { name: "openid_connect", @@ -203,7 +203,7 @@ Example Omnibus configuration block: ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Azure OIDC", # optional label for login button, defaults to "Openid Connect" args: { name: "openid_connect", @@ -335,7 +335,7 @@ but `LocalAccounts` authenticates against local Active Directory accounts. Befor ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Azure B2C OIDC", # optional label for login button, defaults to "Openid Connect" args: { name: "openid_connect", @@ -395,7 +395,7 @@ Example Omnibus configuration block: ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Keycloak", # optional label for login button, defaults to "Openid Connect" args: { name: "openid_connect", @@ -475,7 +475,7 @@ To use symmetric key encryption: ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Keycloak", # optional label for login button, defaults to "Openid Connect" args: { name: "openid_connect", @@ -519,7 +519,7 @@ Example Omnibus GitLab configuration (file path: `/etc/gitlab/gitlab.rb`): ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "openid_connect", + name: "openid_connect", # do not change this parameter label: "Casdoor", # optional label for login button, defaults to "Openid Connect" args: { name: "openid_connect", @@ -542,7 +542,7 @@ gitlab_rails['omniauth_providers'] = [ Example installations from source configuration (file path: `config/gitlab.yml`): ```yaml - - { name: 'openid_connect', + - { name: 'openid_connect', # do not change this parameter label: 'Casdoor', # optional label for login button, defaults to "Openid Connect" args: { name: 'openid_connect', diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index dd3da0251d8..61b7f623660 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -26609,6 +26609,7 @@ Attributes for defining a CI/CD variable. | <a id="complianceviolationinputmergedafter"></a>`mergedAfter` | [`Date`](#date) | Merge requests merged after this date (inclusive). | | <a id="complianceviolationinputmergedbefore"></a>`mergedBefore` | [`Date`](#date) | Merge requests merged before this date (inclusive). | | <a id="complianceviolationinputprojectids"></a>`projectIds` | [`[ProjectID!]`](#projectid) | Filter compliance violations by project. | +| <a id="complianceviolationinputtargetbranch"></a>`targetBranch` | [`String`](#string) | Filter compliance violations by target branch. | ### `DastProfileCadenceInput` diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index 40c6a0c1435..2c5e309348c 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -12,7 +12,7 @@ When a pipeline job is about to run, GitLab generates a unique token and injects You can use a GitLab CI/CD job token to authenticate with specific API endpoints: - Packages: - - [Package Registry](../../user/packages/package_registry/index.md#use-gitlab-cicd-to-build-packages). + - [Package Registry](../../user/packages/package_registry/index.md#to-build-packages). - [Packages API](../../api/packages.md) (project-level). - [Container Registry](../../user/packages/container_registry/build_and_push_images.md#use-gitlab-cicd) (the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`). @@ -76,6 +76,10 @@ be accessed unless projects are explicitly authorized. There is a proposal to add more strategic control of the access permissions, see [epic 3559](https://gitlab.com/groups/gitlab-org/-/epics/3559). +NOTE: +Because `CI_REGISTRY_TOKEN` uses `CI_JOB_TOKEN` to authenticate, the access configuration +also applies to `CI_REGISTRY_TOKEN`. + ### Allow access to your project with a job token > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/346298/) in GitLab 15.9. [Deployed behind the `:inbound_ci_scoped_job_token` feature flag](../../user/feature_flags.md), enabled by default. diff --git a/doc/ci/yaml/includes.md b/doc/ci/yaml/includes.md index 252cef0aa97..94148400896 100644 --- a/doc/ci/yaml/includes.md +++ b/doc/ci/yaml/includes.md @@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# GitLab CI/CD include examples **(FREE)** +# Use CI/CD configuration from other files **(FREE)** You can use [`include`](index.md#include) to include external YAML files in your CI/CD jobs. @@ -503,6 +503,88 @@ When the pipeline runs, GitLab: include: 'configs/**/*.yml' ``` +## Define inputs for configuration added with `include` (Beta) + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/391331) in GitLab 15.11 as a Beta feature. + +FLAG: +`spec` and `with` are experimental [Open Beta features](../../policy/alpha-beta-support.md#beta) +and subject to change without notice. + +### Define input parameters with `spec:inputs` + +Use `spec:inputs` to define input parameters for CI/CD configuration intended to be added +to a pipeline with `include`. Use [`include:with`](#set-input-parameter-values-with-includewith) +to define the values to use when the pipeline runs. + +The specs must be declared at the top of the configuration file, in a header section. +Separate the header from the rest of the configuration with `---`. + +Use the interpolation format `$[[ input.input-id ]]` to reference the values outside of the header section. +The inputs are evaluated and interpolated once, when the configuration is fetched +during pipeline creation, but before the configuration is merged with the contents of the `.gitlab-ci.yml`. + +```yaml +spec: + inputs: + environment: + job-stage: +--- + +scan-website: + stage: $[[ inputs.job-stage ]] + script: ./scan-website $[[ inputs.environment ]] +``` + +When using `spec:inputs`: + +- Defined inputs are mandatory by default. +- Inputs can be made optional by specifying a `default`. Use `default: null` to have no default value. +- A string containing an interpolation block must not exceed 1 MB. +- The string inside an interpolation block must not exceed 1 KB. + +For example, a `custom_configuration.yml`: + +```yaml +spec: + inputs: + website: + user: + default: 'test-user' + flags: + default: null +--- + +# The pipeline configuration would follow... +``` + +In this example: + +- `website` is mandatory and must be defined. +- `user` is optional. If not defined, the value is `test-user`. +- `flags` is optional. If not defined, it has no value. + +### Set input parameter values with `include:with` + +Use `include:with` to set the values for the parameters when the included configuration +is added to the pipeline. + +For example, to include a `custom_configuration.yml` that has the same specs +as the [example above](#define-input-parameters-with-specinputs): + +```yaml +include: + - local: 'custom_configuration.yml' + with: + website: "My website" +``` + +In this example: + +- `website` has a value of `My website` for the included configuration. +- `user` has a value of `test-user`, because that is the default when not specified. +- `flags` has no value, because it is optional and has no default when not specified. + ## Troubleshooting ### `Maximum of 150 nested includes are allowed!` error diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index cd4dc6b21e4..2af9d12a251 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -315,6 +315,27 @@ NOTE: WARNING: `stub_method` is supposed to be used in factories only. It's strongly discouraged to be used elsewhere. Please consider using [RSpec mocks](https://rspec.info/features/3-12/rspec-mocks/) if available. +#### Stubbing member access level + +To stub [member access level](../../user/permissions.md#roles) for factory stubs like `Project` or `Group` use +[`stub_member_access_level`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/support/stub_member_access_level.rb): + +```ruby +let(:project) { build_stubbed(:project) } +let(:maintainer) { build_stubbed(:user) } +let(:policy) { ProjectPolicy.new(maintainer, project) } + +it 'allows admin_project ability' do + stub_member_access_level(project, maintainer: maintainer) + + expect(policy).to be_allowed(:admin_project) +end +``` + +NOTE: +Refrain from using this stub helper if the test code relies on persisting +`project_authorizations` or `Member` records. Use `Project#add_member` or `Group#add_member` instead. + #### Identify slow tests Running a spec with profiling is a good way to start optimizing a spec. This can diff --git a/doc/operations/incident_management/incident_timeline_events.md b/doc/operations/incident_management/incident_timeline_events.md index fdbb4844bb7..d509061eca0 100644 --- a/doc/operations/incident_management/incident_timeline_events.md +++ b/doc/operations/incident_management/incident_timeline_events.md @@ -83,6 +83,17 @@ of an incident. ![Incident timeline event for severity change](img/timeline_event_for_severity_change_v15_6.png) +### When labels change (Experiment) + +> [Introduced]([issue-link](https://gitlab.com/gitlab-org/gitlab/-/issues/365489)) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `incident_timeline_events_from_labels`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `incident_timeline_events_from_labels`. +On GitLab.com, this feature is not available. +This feature is not ready for production use. + +A new timeline event is created when someone adds or removes [labels](../../user/project/labels.md) on an incident. + ## Delete an event > Ability to delete an event when editing it [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/372265) in GitLab 15.7. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 40f482c9fb9..d38e7f59f89 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -1043,15 +1043,16 @@ To be prepared for this change, you should do the following before GitLab 16.0: <div class="deprecation breaking-change" data-milestone="16.0"> -### Jira GitHub Enterprise DVCS integration +### Jira DVCS connector for Jira Cloud <div class="deprecation-notes"> - Announced in: GitLab <span class="milestone">15.1</span> - [Breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/) </div> -The [Jira DVCS Connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) (which enables the [Jira Development Panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/)), will no longer support Jira Cloud users starting with GitLab 16.0. The [GitLab for Jira App](https://docs.gitlab.com/ee/integration/jira/connect-app.html) has always been recommended for Jira Cloud users, and it will be required instead of the DVCS connector. If you are a Jira Cloud user, we recommended you begin migrating to the GitLab for Jira App. -Any Jira Server and Jira Data Center users will need to confirm they are not using the GitHub Enterprise Connector to enable the GitLab DVCS integration, but they may continue to use the [native GitLab DVCS integration](https://docs.gitlab.com/ee/integration/jira/dvcs/) (supported in Jira 8.14 and later). +The [Jira DVCS connector](https://docs.gitlab.com/ee/integration/jira/dvcs/) for Jira Cloud has been deprecated and will be removed in GitLab 16.0. If you're using the Jira DVCS connector with Jira Cloud, migrate to the [GitLab for Jira Cloud app](https://docs.gitlab.com/ee/integration/jira/connect-app.html). + +The Jira DVCS connector is also deprecated for Jira 8.13 and earlier. You can only use the Jira DVCS connector with Jira Server or Jira Data Center in Jira 8.14 and later. </div> diff --git a/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md b/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md index e2b2fc16707..ce0c6451688 100644 --- a/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md +++ b/doc/user/compliance/license_scanning_of_cyclonedx_files/index.md @@ -8,8 +8,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w # License scanning of CycloneDX files **(ULTIMATE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384932) in GitLab 15.9 for GitLab SaaS [with two flags](../../../administration/feature_flags.md) named `license_scanning_sbom_scanner` and `package_metadata_synchronization`. Both flags are disabled by default and both flags must be enabled for this feature to work. -> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/385173) in GitLab 15.10. +> - [Enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/385173) in GitLab 15.10 for GitLab SaaS. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385173) in GitLab 15.10 for self-managed GitLab [with two flags](../../../administration/feature_flags.md) named `license_scanning_sbom_scanner` and `package_metadata_synchronization`, both of which must be enabled for this feature to work. The flags are disabled by default due to the initial performance load when the license database is first imported. Work to improve performance is being tracked in [issue 397670](https://gitlab.com/gitlab-org/gitlab/-/issues/397670). +> - [Enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/385173) in GitLab 15.11 for self-managed GitLab. To detect the licenses in use, License Compliance relies on running the [Dependency Scanning CI Jobs](../../application_security/dependency_scanning/index.md), diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md index cd60a229479..eb3fd082d33 100644 --- a/doc/user/packages/package_registry/index.md +++ b/doc/user/packages/package_registry/index.md @@ -71,9 +71,13 @@ NOTE: If you have not activated the "Package registry" feature for your project at **Settings > General > Visibility, project features, permissions**, you receive a 403 Forbidden response. Accessing package registry via deploy token is not available when external authorization is enabled. -## Use GitLab CI/CD to build packages +## Use GitLab CI/CD + +You can use [GitLab CI/CD](../../../ci/index.md) to build or import packages into +a package registry. + +### To build packages -You can use [GitLab CI/CD](../../../ci/index.md) to build packages. For Maven, NuGet, npm, Conan, Helm, and PyPI packages, and Composer dependencies, you can authenticate with GitLab by using the `CI_JOB_TOKEN`. @@ -97,6 +101,16 @@ when you view the package details: You can view which pipeline published the package, and the commit and user who triggered it. However, the history is limited to five updates of a given package. +### To import packages + +If you already have packages built in a different registry, you can import them +into your GitLab package registry with the [Packages Importer](https://gitlab.com/gitlab-org/ci-cd/package-stage/pkgs_importer) + +The Packages Importer runs a CI/CD pipeline that [can import these package types](https://gitlab.com/gitlab-org/ci-cd/package-stage/pkgs_importer#formats-supported): + +- NPM +- NuGet + ## Reduce storage usage For information on reducing your storage use for the Package Registry, see diff --git a/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb b/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb index 6f5ddec628d..2127ce5975d 100644 --- a/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb +++ b/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens.rb @@ -19,7 +19,11 @@ module Gitlab def perform each_sub_batch do |sub_batch| sub_batch.each do |token| - token.update!(scopes: (YAML.safe_load(token.scopes) + ADMIN_MODE_SCOPE).uniq.to_yaml) + existing_scopes = YAML.safe_load(token.scopes, permitted_classes: [Symbol]) + # making sure scopes are not mixed symbols and strings + stringified_scopes = existing_scopes.map(&:to_s) + + token.update!(scopes: (stringified_scopes + ADMIN_MODE_SCOPE).uniq.to_yaml) end end end diff --git a/lib/gitlab/git/blame_pagination.rb b/lib/gitlab/git/blame_pagination.rb new file mode 100644 index 00000000000..6bf29859b14 --- /dev/null +++ b/lib/gitlab/git/blame_pagination.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class BlamePagination + include Gitlab::Utils::StrongMemoize + + PAGINATION_PER_PAGE = 1000 + STREAMING_FIRST_PAGE_SIZE = 200 + STREAMING_PER_PAGE = 2000 + + def initialize(blob, blame_mode, params) + @blob = blob + @blame_mode = blame_mode + @params = params + end + + def page + page = params.fetch(:page, 1).to_i + + return 1 if page < 1 + + page + end + strong_memoize_attr :page + + def per_page + blame_mode.streaming? ? STREAMING_PER_PAGE : PAGINATION_PER_PAGE + end + strong_memoize_attr :per_page + + def total_pages + total = (blob_lines_count.to_f / per_page).ceil + return total unless blame_mode.streaming? + + ([blob_lines_count - STREAMING_FIRST_PAGE_SIZE, 0].max.to_f / per_page).ceil + 1 + end + strong_memoize_attr :total_pages + + def total_extra_pages + [total_pages - 1, 0].max + end + strong_memoize_attr :total_extra_pages + + def paginator + return if blame_mode.streaming? || blame_mode.full? + + Kaminari.paginate_array([], total_count: blob_lines_count, limit: per_page) + .tap { |pagination| pagination.max_paginates_per(per_page) } + .page(page) + end + + def blame_range + return if blame_mode.full? + + first_line = ((page - 1) * per_page) + 1 + + if blame_mode.streaming? + return 1..STREAMING_FIRST_PAGE_SIZE if page == 1 + + first_line = STREAMING_FIRST_PAGE_SIZE + ((page - 2) * per_page) + 1 + end + + last_line = (first_line + per_page).to_i - 1 + + first_line..last_line + end + + private + + attr_reader :blob, :blame_mode, :params + + def blob_lines_count + @blob_lines_count ||= blob.data.lines.count + end + end + end +end diff --git a/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb b/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb index bc4c69c461a..c79e7e379d0 100644 --- a/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb +++ b/lib/sidebars/groups/super_sidebar_menus/secure_menu.rb @@ -17,9 +17,9 @@ module Sidebars override :configure_menu_items def configure_menu_items [ - :audit_events, :security_dashboard, :vulnerability_report, + :audit_events, :compliance, :scan_policies ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb index 980f13d81bd..31a1aa56ab5 100644 --- a/lib/sidebars/projects/menus/packages_registries_menu.rb +++ b/lib/sidebars/projects/menus/packages_registries_menu.rb @@ -10,6 +10,7 @@ module Sidebars add_item(container_registry_menu_item) add_item(infrastructure_registry_menu_item) add_item(harbor_registry_menu_item) + add_item(model_experiments_menu_item) true end @@ -89,6 +90,20 @@ module Sidebars ) end + def model_experiments_menu_item + if Feature.disabled?(:ml_experiment_tracking, context.project) + return ::Sidebars::NilMenuItem.new(item_id: :model_experiments) + end + + ::Sidebars::MenuItem.new( + title: _('Model experiments'), + link: project_ml_experiments_path(context.project), + super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu, + active_routes: { controller: %w[projects/ml/experiments projects/ml/candidates] }, + item_id: :model_experiments + ) + end + def packages_registry_disabled? !::Gitlab.config.packages.enabled || !can?(context.current_user, :read_package, context.project&.packages_policy_subject) diff --git a/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb b/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb index 2d1b4c0c495..78a988fffaf 100644 --- a/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb +++ b/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb @@ -25,7 +25,8 @@ module Sidebars :code_review, :merge_requests, :issues, - :insights + :insights, + :model_experiments ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } end end diff --git a/lib/sidebars/projects/super_sidebar_menus/secure_menu.rb b/lib/sidebars/projects/super_sidebar_menus/secure_menu.rb index 8639e3dbb1d..2ca53ba0ce9 100644 --- a/lib/sidebars/projects/super_sidebar_menus/secure_menu.rb +++ b/lib/sidebars/projects/super_sidebar_menus/secure_menu.rb @@ -18,13 +18,13 @@ module Sidebars def configure_menu_items [ :discover_project_security, - :audit_events, :dashboard, :vulnerability_report, - :on_demand_scans, - :scan_policies, :dependency_list, :license_compliance, + :audit_events, + :scan_policies, + :on_demand_scans, :configuration ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index a58eea0d880..963fe23c682 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -109,9 +109,11 @@ namespace :gitlab do load_database = connection.tables.count <= 1 if load_database + puts "Running db:schema:load#{database_name} rake task" Gitlab::Database.add_post_migrate_path_to_rails(force: true) Rake::Task["db:schema:load#{database_name}"].invoke else + puts "Running db:migrate#{database_name} rake task" Rake::Task["db:migrate#{database_name}"].invoke end diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index a2417128c48..9d99625140e 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -22,6 +22,7 @@ namespace :tw do # CodeOwnerRule.new('Activation', ''), # CodeOwnerRule.new('Acquisition', ''), # CodeOwnerRule.new('AI Assisted', ''), + CodeOwnerRule.new('Analytics Instrumentation', '@lciutacu'), CodeOwnerRule.new('Anti-Abuse', '@phillipwells'), CodeOwnerRule.new('Application Performance', '@jglassman1'), CodeOwnerRule.new('Authentication and Authorization', '@jglassman1'), @@ -62,7 +63,6 @@ namespace :tw do CodeOwnerRule.new('Pipeline Execution', '@drcatherinepope'), CodeOwnerRule.new('Pipeline Security', '@marcel.amirault'), CodeOwnerRule.new('Product Analytics', '@lciutacu'), - CodeOwnerRule.new('Product Intelligence', '@lciutacu'), CodeOwnerRule.new('Product Planning', '@msedlakjakubowski'), CodeOwnerRule.new('Project Management', '@msedlakjakubowski'), CodeOwnerRule.new('Provision', '@fneill'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f3b9376b213..2324724a30c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13084,9 +13084,18 @@ msgstr "" msgid "DNS" msgstr "" +msgid "DORA4Metrics|Accept testing terms" +msgstr "" + +msgid "DORA4Metrics|Accept testing terms of use?" +msgstr "" + msgid "DORA4Metrics|Average (last %{days}d)" msgstr "" +msgid "DORA4Metrics|By enabling this feature, you accept the %{url}" +msgstr "" + msgid "DORA4Metrics|Change Failure Rate" msgstr "" @@ -13120,6 +13129,9 @@ msgstr "" msgid "DORA4Metrics|Failed to load charts" msgstr "" +msgid "DORA4Metrics|Forecast" +msgstr "" + msgid "DORA4Metrics|Go to docs" msgstr "" @@ -13162,9 +13174,6 @@ msgstr "" msgid "DORA4Metrics|No merge requests were deployed during this period" msgstr "" -msgid "DORA4Metrics|Number of deployments" -msgstr "" - msgid "DORA4Metrics|Number of incidents divided by the number of deployments to a production environment in the given time period." msgstr "" @@ -13174,6 +13183,12 @@ msgstr "" msgid "DORA4Metrics|Percentage of failed deployments" msgstr "" +msgid "DORA4Metrics|Predicted number of deployments" +msgstr "" + +msgid "DORA4Metrics|Show forecast" +msgstr "" + msgid "DORA4Metrics|Something went wrong while getting change failure rate data." msgstr "" @@ -17457,6 +17472,9 @@ msgstr "" msgid "Expected documents: %{expected_documents}" msgstr "" +msgid "Experiment" +msgstr "" + msgid "Experiments" msgstr "" @@ -28355,6 +28373,9 @@ msgstr "" msgid "Modal|Close" msgstr "" +msgid "Model experiments" +msgstr "" + msgid "Modified" msgstr "" diff --git a/spec/factories/clusters/agents/authorizations/user_access/group_authorizations.rb b/spec/factories/clusters/agents/authorizations/user_access/group_authorizations.rb new file mode 100644 index 00000000000..203aadbd741 --- /dev/null +++ b/spec/factories/clusters/agents/authorizations/user_access/group_authorizations.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :agent_user_access_group_authorization, + class: 'Clusters::Agents::Authorizations::UserAccess::GroupAuthorization' do + association :agent, factory: :cluster_agent + config { {} } + group + end +end diff --git a/spec/factories/clusters/agents/authorizations/user_access/project_authorizations.rb b/spec/factories/clusters/agents/authorizations/user_access/project_authorizations.rb new file mode 100644 index 00000000000..8171607f578 --- /dev/null +++ b/spec/factories/clusters/agents/authorizations/user_access/project_authorizations.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :agent_user_access_project_authorization, + class: 'Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization' do + association :agent, factory: :cluster_agent + config { {} } + project + end +end diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index 95f25d5c23d..8e80ce5edd9 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -311,6 +311,40 @@ RSpec.describe 'Admin::Users', feature_category: :user_management do end end + describe 'users pending approval' do + it 'sends a welcome email and a password reset email to the user upon admin approval', :sidekiq_inline do + user = create(:user, :blocked_pending_approval, created_by_id: current_user.id) + + visit admin_users_path + + click_link 'Pending approval' + + click_user_dropdown_toggle(user.id) + + find('[data-testid="approve"]').click + + expect(page).to have_content("Approve user #{user.name}?") + + within_modal do + perform_enqueued_jobs do + click_button 'Approve' + end + end + + expect(page).to have_content('Successfully approved') + + welcome_email = ActionMailer::Base.deliveries.find { |m| m.subject == 'Welcome to GitLab!' } + expect(welcome_email.to).to eq([user.email]) + expect(welcome_email.text_part.body).to have_content('Your GitLab account request has been approved!') + + password_reset_email = ActionMailer::Base.deliveries.find { |m| m.subject == 'Account was created for you' } + expect(password_reset_email.to).to eq([user.email]) + expect(password_reset_email.text_part.body).to have_content('Click here to set your password') + + expect(ActionMailer::Base.deliveries.count).to eq(2) + end + end + describe 'internal users' do context 'when showing a `Ghost User`' do let_it_be(:ghost_user) { create(:user, :ghost) } diff --git a/spec/features/projects/blobs/blame_spec.rb b/spec/features/projects/blobs/blame_spec.rb index d3558af81b8..6f5bf8ac26e 100644 --- a/spec/features/projects/blobs/blame_spec.rb +++ b/spec/features/projects/blobs/blame_spec.rb @@ -44,7 +44,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do context 'when blob length is over the blame range limit' do before do - stub_const('Projects::BlameService::PER_PAGE', 2) + stub_const('Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE', 2) end it 'displays two first lines of the file with pagination' do @@ -112,7 +112,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do context 'when streaming is enabled' do before do - stub_const('Projects::BlameService::STREAMING_PER_PAGE', 50) + stub_const('Gitlab::Git::BlamePagination::STREAMING_PER_PAGE', 50) end it_behaves_like 'a full blame page' @@ -143,7 +143,7 @@ RSpec.describe 'File blame', :js, feature_category: :projects do context 'when blob length is over global max page limit' do before do - stub_const('Projects::BlameService::PER_PAGE', 200) + stub_const('Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE', 200) end let(:path) { 'files/markdown/ruby-style-guide.md' } diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 521f633e0e7..31f4e9dcf95 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :projects do stub_config(registry: { enabled: false }) stub_feature_flags(harbor_registry_integration: false) + stub_feature_flags(ml_experiment_tracking: false) insert_package_nav(_('Deployments')) insert_infrastructure_registry_nav insert_infrastructure_google_cloud_nav @@ -98,4 +99,16 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :projects do it_behaves_like 'verified navigation bar' end + + context 'when models experiments is available' do + before do + stub_feature_flags(ml_experiment_tracking: true) + + insert_model_experiments_nav(_('Terraform modules')) + + visit project_path(project) + end + + it_behaves_like 'verified navigation bar' + end end diff --git a/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb b/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb index d2da6867773..92fec48454c 100644 --- a/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_admin_mode_scope_for_personal_access_tokens_spec.rb @@ -24,8 +24,12 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillAdminModeScopeForPersonalAcc personal_access_tokens.create!(name: 'admin 4', user_id: admin.id, scopes: "---\n- admin_mode\n") end - let!(:pat_admin_2) { personal_access_tokens.create!(name: 'admin 5', user_id: admin.id, scopes: "---\n- read_api\n") } - let!(:pat_not_in_range) { personal_access_tokens.create!(name: 'admin 6', user_id: admin.id, scopes: "---\n- api\n") } + let!(:pat_with_symbol_in_scopes) do + personal_access_tokens.create!(name: 'admin 5', user_id: admin.id, scopes: "---\n- :api\n") + end + + let!(:pat_admin_2) { personal_access_tokens.create!(name: 'admin 6', user_id: admin.id, scopes: "---\n- read_api\n") } + let!(:pat_not_in_range) { personal_access_tokens.create!(name: 'admin 7', user_id: admin.id, scopes: "---\n- api\n") } subject do described_class.new( @@ -47,6 +51,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillAdminModeScopeForPersonalAcc expect(pat_revoked.reload.scopes).to eq("---\n- api\n") expect(pat_expired.reload.scopes).to eq("---\n- api\n") expect(pat_admin_mode.reload.scopes).to eq("---\n- admin_mode\n") + expect(pat_with_symbol_in_scopes.reload.scopes).to eq("---\n- api\n- admin_mode\n") expect(pat_admin_2.reload.scopes).to eq("---\n- read_api\n- admin_mode\n") expect(pat_not_in_range.reload.scopes).to eq("---\n- api\n") end diff --git a/spec/lib/gitlab/git/blame_pagination_spec.rb b/spec/lib/gitlab/git/blame_pagination_spec.rb new file mode 100644 index 00000000000..1f3c0c0342e --- /dev/null +++ b/spec/lib/gitlab/git/blame_pagination_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Git::BlamePagination, feature_category: :source_code_management do + subject(:blame_pagination) { described_class.new(blob, blame_mode, params) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:commit) { project.repository.commit } + let_it_be(:blob) { project.repository.blob_at('HEAD', 'README.md') } + + let(:blame_mode) do + instance_double( + 'Gitlab::Git::BlameMode', + 'streaming?' => streaming_mode, + 'full?' => full_mode + ) + end + + let(:params) { { page: page } } + let(:page) { 1 } + let(:streaming_mode) { false } + let(:full_mode) { false } + + using RSpec::Parameterized::TableSyntax + + describe '#page' do + subject { blame_pagination.page } + + where(:page, :expected_page) do + nil | 1 + 1 | 1 + 5 | 5 + -1 | 1 + 'a' | 1 + end + + with_them do + it { is_expected.to eq(expected_page) } + end + end + + describe '#per_page' do + subject { blame_pagination.per_page } + + it { is_expected.to eq(described_class::PAGINATION_PER_PAGE) } + + context 'when blame mode is streaming' do + let(:streaming_mode) { true } + + it { is_expected.to eq(described_class::STREAMING_PER_PAGE) } + end + end + + describe '#total_pages' do + subject { blame_pagination.total_pages } + + before do + stub_const("#{described_class.name}::PAGINATION_PER_PAGE", 2) + end + + it { is_expected.to eq(2) } + end + + describe '#total_extra_pages' do + subject { blame_pagination.total_extra_pages } + + before do + stub_const("#{described_class.name}::PAGINATION_PER_PAGE", 2) + end + + it { is_expected.to eq(1) } + end + + describe '#pagination' do + subject { blame_pagination.paginator } + + before do + stub_const("#{described_class.name}::PAGINATION_PER_PAGE", 2) + end + + it 'returns a pagination object' do + is_expected.to be_kind_of(Kaminari::PaginatableArray) + + expect(subject.current_page).to eq(1) + expect(subject.total_pages).to eq(2) + expect(subject.total_count).to eq(4) + end + + context 'when user disabled the pagination' do + let(:full_mode) { true } + + it { is_expected.to be_nil } + end + + context 'when user chose streaming' do + let(:streaming_mode) { true } + + it { is_expected.to be_nil } + end + + context 'when per_page is above the global max per page limit' do + before do + stub_const("#{described_class.name}::PAGINATION_PER_PAGE", 1000) + allow(blob).to receive_message_chain(:data, :lines, :count) { 500 } + end + + it 'returns a correct pagination object' do + is_expected.to be_kind_of(Kaminari::PaginatableArray) + + expect(subject.current_page).to eq(1) + expect(subject.total_pages).to eq(1) + expect(subject.total_count).to eq(500) + end + end + + describe 'Pagination attributes' do + where(:page, :current_page, :total_pages) do + 1 | 1 | 2 + 2 | 2 | 2 + 0 | 1 | 2 # Incorrect + end + + with_them do + it 'returns the correct pagination attributes' do + expect(subject.current_page).to eq(current_page) + expect(subject.total_pages).to eq(total_pages) + end + end + end + end + + describe '#blame_range' do + subject { blame_pagination.blame_range } + + before do + stub_const("#{described_class.name}::PAGINATION_PER_PAGE", 2) + end + + where(:page, :expected_range) do + 1 | (1..2) + 2 | (3..4) + 0 | (1..2) + end + + with_them do + it { is_expected.to eq(expected_range) } + end + + context 'when user disabled the pagination' do + let(:full_mode) { true } + + it { is_expected.to be_nil } + end + + context 'when streaming is enabled' do + let(:streaming_mode) { true } + + before do + stub_const("#{described_class.name}::STREAMING_FIRST_PAGE_SIZE", 1) + stub_const("#{described_class.name}::STREAMING_PER_PAGE", 1) + end + + where(:page, :expected_range) do + 1 | (1..1) + 2 | (2..2) + 0 | (1..1) + end + + with_them do + it { is_expected.to eq(expected_range) } + end + end + end +end diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/secure_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/secure_menu_spec.rb index 3640a4d8655..9eb81dda462 100644 --- a/spec/lib/sidebars/groups/super_sidebar_menus/secure_menu_spec.rb +++ b/spec/lib/sidebars/groups/super_sidebar_menus/secure_menu_spec.rb @@ -15,9 +15,9 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::SecureMenu, feature_category it 'defines list of NilMenuItem placeholders' do expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ - :audit_events, :security_dashboard, :vulnerability_report, + :audit_events, :compliance, :scan_policies ]) diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb index 6d7698db743..860206dc6af 100644 --- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu, feature_catego before do stub_container_registry_config(enabled: registry_enabled) stub_config(packages: { enabled: packages_enabled }) - stub_feature_flags(harbor_registry_integration: false) + stub_feature_flags(harbor_registry_integration: false, ml_experiment_tracking: false) end context 'when Packages Registry is visible' do @@ -181,5 +181,25 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu, feature_catego end end end + + describe 'Model experiments' do + let(:item_id) { :model_experiments } + + context 'when :ml_experiment_tracking is enabled' do + it 'shows the menu item' do + stub_feature_flags(ml_experiment_tracking: true) + + is_expected.not_to be_nil + end + end + + context 'when :ml_experiment_tracking is disabled' do + it 'does not show the menu item' do + stub_feature_flags(ml_experiment_tracking: false) + + is_expected.to be_nil + end + end + end end end diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb index b8d74665042..8f07241d2e2 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb @@ -23,7 +23,8 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu, feature_categ :code_review, :merge_requests, :issues, - :insights + :insights, + :model_experiments ]) end end diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/secure_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/secure_menu_spec.rb index b68b33941c3..74ef761332e 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/secure_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/secure_menu_spec.rb @@ -16,13 +16,13 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::SecureMenu, feature_catego expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ :discover_project_security, - :audit_events, :dashboard, :vulnerability_report, - :on_demand_scans, - :scan_policies, :dependency_list, :license_compliance, + :audit_events, + :scan_policies, + :on_demand_scans, :configuration ]) end diff --git a/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb b/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb deleted file mode 100644 index 068da23113d..00000000000 --- a/spec/migrations/queue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require_migration! - -RSpec.describe QueueBackfillAdminModeScopeForPersonalAccessTokens, - feature_category: :system_access do - describe '#up' do - it 'schedules background migration' do - migrate! - - expect(described_class::MIGRATION).to have_scheduled_batched_migration( - table_name: :personal_access_tokens, - column_name: :id, - interval: described_class::DELAY_INTERVAL) - end - end -end diff --git a/spec/migrations/requeue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb b/spec/migrations/requeue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb new file mode 100644 index 00000000000..b9af6d98beb --- /dev/null +++ b/spec/migrations/requeue_backfill_admin_mode_scope_for_personal_access_tokens_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RequeueBackfillAdminModeScopeForPersonalAccessTokens, feature_category: :system_access do + describe '#up' do + it 'schedules background migration' do + migrate! + + expect(described_class::MIGRATION).to( + have_scheduled_batched_migration( + table_name: :personal_access_tokens, + column_name: :id, + interval: described_class::DELAY_INTERVAL) + ) + end + end +end diff --git a/spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb b/spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb new file mode 100644 index 00000000000..d7b4ea2388f --- /dev/null +++ b/spec/models/clusters/agents/authorizations/user_access/group_authorization_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::Agents::Authorizations::UserAccess::GroupAuthorization, feature_category: :kubernetes_management do + it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required } + it { is_expected.to belong_to(:group).class_name('::Group').required } + + it { expect(described_class).to validate_jsonb_schema(['config']) } + + describe '#config_project' do + let(:record) { create(:agent_user_access_group_authorization) } + + it { expect(record.config_project).to eq(record.agent.project) } + end +end diff --git a/spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb b/spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb new file mode 100644 index 00000000000..a51be15a9d7 --- /dev/null +++ b/spec/models/clusters/agents/authorizations/user_access/project_authorization_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Clusters::Agents::Authorizations::UserAccess::ProjectAuthorization, feature_category: :kubernetes_management do + it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required } + it { is_expected.to belong_to(:project).class_name('Project').required } + + it { expect(described_class).to validate_jsonb_schema(['config']) } + + describe '#config_project' do + let(:record) { create(:agent_user_access_project_authorization) } + + it { expect(record.config_project).to eq(record.agent.project) } + end +end diff --git a/spec/presenters/issue_email_participant_presenter_spec.rb b/spec/presenters/issue_email_participant_presenter_spec.rb index c270fae3058..993cc9c235b 100644 --- a/spec/presenters/issue_email_participant_presenter_spec.rb +++ b/spec/presenters/issue_email_participant_presenter_spec.rb @@ -3,54 +3,49 @@ require 'spec_helper' RSpec.describe IssueEmailParticipantPresenter, feature_category: :service_desk do - # See https://gitlab.com/gitlab-org/gitlab/-/issues/389247 - # for details around build_stubbed for access level - let_it_be(:non_member) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate - let_it_be(:guest) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate - let_it_be(:reporter) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate - let_it_be(:developer) { create(:user) } # rubocop:todo RSpec/FactoryBot/AvoidCreate - let_it_be(:group) { create(:group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate - let_it_be(:project) { create(:project, group: group) } # rubocop:todo RSpec/FactoryBot/AvoidCreate - let_it_be(:issue) { build_stubbed(:issue, project: project) } - let_it_be(:participant) { build_stubbed(:issue_email_participant, issue: issue, email: 'any@email.com') } - - let(:user) { nil } - let(:presenter) { described_class.new(participant, current_user: user) } + let(:user) { build_stubbed(:user) } + let(:project) { build_stubbed(:project) } + let(:issue) { build_stubbed(:issue, project: project) } + let(:participant) { build_stubbed(:issue_email_participant, issue: issue, email: 'any@example.com') } let(:obfuscated_email) { 'an*****@e*****.c**' } - let(:email) { 'any@email.com' } + let(:email) { 'any@example.com' } - before_all do - group.add_guest(guest) - group.add_reporter(reporter) - group.add_developer(developer) - end + subject(:presenter) { described_class.new(participant, current_user: user) } describe '#email' do subject { presenter.email } - it { is_expected.to eq(obfuscated_email) } + context 'when anonymous' do + let(:user) { nil } + + it { is_expected.to eq(obfuscated_email) } + end context 'with signed in user' do + before do + stub_member_access_level(project, access_level => user) if access_level + end + context 'when user has no role in project' do - let(:user) { non_member } + let(:access_level) { nil } it { is_expected.to eq(obfuscated_email) } end context 'when user has guest role in project' do - let(:user) { guest } + let(:access_level) { :guest } it { is_expected.to eq(obfuscated_email) } end context 'when user has reporter role in project' do - let(:user) { reporter } + let(:access_level) { :reporter } it { is_expected.to eq(email) } end context 'when user has developer role in project' do - let(:user) { developer } + let(:access_level) { :developer } it { is_expected.to eq(email) } end diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb index dfe4a191ae5..4727bce02a5 100644 --- a/spec/presenters/project_clusterable_presenter_spec.rb +++ b/spec/presenters/project_clusterable_presenter_spec.rb @@ -2,15 +2,15 @@ require 'spec_helper' -RSpec.describe ProjectClusterablePresenter do +RSpec.describe ProjectClusterablePresenter, feature_category: :environment_management do include Gitlab::Routing.url_helpers let(:presenter) { described_class.new(project) } - let(:project) { create(:project) } - let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } + let(:project) { build_stubbed(:project) } + let(:cluster) { build_stubbed(:cluster, :provided_by_gcp, projects: [project]) } describe '#can_create_cluster?' do - let(:user) { create(:user) } + let(:user) { build_stubbed(:user) } subject { presenter.can_create_cluster? } @@ -20,7 +20,7 @@ RSpec.describe ProjectClusterablePresenter do context 'when user can create' do before do - project.add_maintainer(user) + stub_member_access_level(project, maintainer: user) end it { is_expected.to be_truthy } diff --git a/spec/services/projects/blame_service_spec.rb b/spec/services/projects/blame_service_spec.rb deleted file mode 100644 index 7f7f48cf622..00000000000 --- a/spec/services/projects/blame_service_spec.rb +++ /dev/null @@ -1,132 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::BlameService, :aggregate_failures, feature_category: :source_code_management do - subject(:service) { described_class.new(blob, commit, blame_mode, params) } - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:commit) { project.repository.commit } - let_it_be(:blob) { project.repository.blob_at('HEAD', 'README.md') } - - let(:blame_mode) { Gitlab::Git::BlameMode.new(project, params) } - let(:params) { { page: page } } - let(:page) { nil } - - before do - stub_const("#{described_class.name}::PER_PAGE", 2) - end - - describe '#blame' do - subject { service.blame } - - it 'returns a correct Gitlab::Blame object' do - is_expected.to be_kind_of(Gitlab::Blame) - - expect(subject.blob).to eq(blob) - expect(subject.commit).to eq(commit) - expect(subject.range).to eq(1..2) - end - - describe 'Pagination range calculation' do - subject { service.blame.range } - - context 'with page = 1' do - let(:page) { 1 } - - it { is_expected.to eq(1..2) } - end - - context 'with page = 2' do - let(:page) { 2 } - - it { is_expected.to eq(3..4) } - end - - context 'with page = 3 (overlimit)' do - let(:page) { 3 } - - it { is_expected.to eq(1..2) } - end - - context 'with page = 0 (incorrect)' do - let(:page) { 0 } - - it { is_expected.to eq(1..2) } - end - - context 'when user disabled the pagination' do - let(:params) { super().merge(no_pagination: 1) } - - it { is_expected.to be_nil } - end - - context 'when feature flag disabled' do - before do - stub_feature_flags(blame_page_pagination: false) - end - - it { is_expected.to be_nil } - end - end - end - - describe '#pagination' do - subject { service.pagination } - - it 'returns a pagination object' do - is_expected.to be_kind_of(Kaminari::PaginatableArray) - - expect(subject.current_page).to eq(1) - expect(subject.total_pages).to eq(2) - expect(subject.total_count).to eq(4) - end - - context 'when user disabled the pagination' do - let(:params) { super().merge(no_pagination: 1) } - - it { is_expected.to be_nil } - end - - context 'when feature flag disabled' do - before do - stub_feature_flags(blame_page_pagination: false) - end - - it { is_expected.to be_nil } - end - - context 'when per_page is above the global max per page limit' do - before do - stub_const("#{described_class.name}::PER_PAGE", 1000) - allow(blob).to receive_message_chain(:data, :lines, :count) { 500 } - end - - it 'returns a correct pagination object' do - is_expected.to be_kind_of(Kaminari::PaginatableArray) - - expect(subject.current_page).to eq(1) - expect(subject.total_pages).to eq(1) - expect(subject.total_count).to eq(500) - end - end - - describe 'Pagination attributes' do - using RSpec::Parameterized::TableSyntax - - where(:page, :current_page, :total_pages) do - 1 | 1 | 2 - 2 | 2 | 2 - 3 | 1 | 2 # Overlimit - 0 | 1 | 2 # Incorrect - end - - with_them do - it 'returns the correct pagination attributes' do - expect(subject.current_page).to eq(current_page) - expect(subject.total_pages).to eq(total_pages) - end - end - end - end -end diff --git a/spec/services/users/approve_service_spec.rb b/spec/services/users/approve_service_spec.rb index 1b063a9ad1c..09379857c38 100644 --- a/spec/services/users/approve_service_spec.rb +++ b/spec/services/users/approve_service_spec.rb @@ -75,6 +75,24 @@ RSpec.describe Users::ApproveService, feature_category: :user_management do expect { subject }.to have_enqueued_mail(DeviseMailer, :user_admin_approval) end + context 'when the user was created via sign up' do + it 'does not send a password reset email' do + expect { subject }.not_to have_enqueued_mail(Notify, :new_user_email) + end + end + + context 'when the user was created by an admin' do + let(:user) { create(:user, :blocked_pending_approval, created_by_id: current_user.id) } + + it 'sends a password reset email' do + allow(user).to receive(:generate_reset_token).and_return(:reset_token) + + expect(Notify).to receive(:new_user_email).with(user.id, :reset_token).and_call_original + + expect { subject }.to have_enqueued_mail(Notify, :new_user_email) + end + end + context 'email confirmation status' do context 'user is unconfirmed' do let(:user) { create(:user, :blocked_pending_approval, :unconfirmed) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 42c97d03d21..c3bddf1a6ae 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -172,6 +172,7 @@ RSpec.configure do |config| config.include RailsHelpers config.include SidekiqMiddleware config.include StubActionCableConnection, type: :channel + config.include StubMemberAccessLevel config.include StubSpamServices config.include SnowplowHelpers config.include RenderedHelpers diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index 15c4b679786..67ea00c6551 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -114,6 +114,14 @@ module NavbarStructureHelper ) end + def insert_model_experiments_nav(within) + insert_after_sub_nav_item( + within, + within: _('Packages and registries'), + new_sub_nav_item_name: _('Model experiments') + ) + end + def project_analytics_sub_nav_item [ _('Value stream'), diff --git a/spec/support/stub_member_access_level.rb b/spec/support/stub_member_access_level.rb new file mode 100644 index 00000000000..62e932ee1fc --- /dev/null +++ b/spec/support/stub_member_access_level.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module StubMemberAccessLevel + # Stubs access level of a member of +object+. + # + # The following types are supported: + # * `Project` - stubs `project.team.max_member_access(user.id)` + # * `Group` - stubs `group.max_member_access_for_user(user)` + # + # @example + # + # stub_member_access_level(project, maintainer: user) + # project.team.max_member_access(user.id) # => Gitlab::Access::MAINTAINER + # + # stub_member_access_level(group, developer: user) + # group.max_member_access_for_user(user) # => Gitlab::Access::DEVELOPER + # + # stub_member_access_level(project, reporter: user, guest: [guest1, guest2]) + # project.team.max_member_access(user.id) # => Gitlab::Access::REPORTER + # project.team.max_member_access(guests.first.id) # => Gitlab::Access::GUEST + # project.team.max_member_access(guests.last.id) # => Gitlab::Access::GUEST + # + # @param object [Project, Group] Object to be stubbed. + # @param access_levels [Hash<Symbol, User>, Hash<Symbol, [User]>] Map of access level to users + def stub_member_access_level(object, **access_levels) + expectation = case object + when Project + ->(user) { expect(object.team).to receive(:max_member_access).with(user.id) } + when Group + ->(user) { expect(object).to receive(:max_member_access_for_user).with(user) } + else + raise ArgumentError, + "Stubbing member access level unsupported for #{object.inspect} (#{object.class})" + end + + access_levels.each do |access_level, users| + access_level = Gitlab::Access.sym_options_with_owner.fetch(access_level) do + raise ArgumentError, "Invalid access level #{access_level.inspect}" + end + + Array(users).each do |user| + expectation.call(user).at_least(1).times.and_return(access_level) + end + end + end +end diff --git a/spec/support_specs/stub_member_access_level_spec.rb b/spec/support_specs/stub_member_access_level_spec.rb new file mode 100644 index 00000000000..c76bd2ee417 --- /dev/null +++ b/spec/support_specs/stub_member_access_level_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_relative '../support/stub_member_access_level' + +RSpec.describe StubMemberAccessLevel, feature_category: :system_access do + include described_class + + describe 'stub_member_access_level' do + shared_examples 'access level stubs' do + let(:guests) { build_stubbed_list(:user, 2) } + let(:maintainer) { build_stubbed(:user) } + let(:no_access) { build_stubbed(:user) } + + it 'stubs max member access level per user' do + stub_member_access_level(object, maintainer: maintainer, guest: guests) + + # Ensure that multple calls are allowed + 2.times do + expect(access_level_for(maintainer)).to eq(Gitlab::Access::MAINTAINER) + expect(access_level_for(guests.first)).to eq(Gitlab::Access::GUEST) + expect(access_level_for(guests.last)).to eq(Gitlab::Access::GUEST) + + # Partially stub so we expect a mock error. + expect { access_level_for(no_access) }.to raise_error(RSpec::Mocks::MockExpectationError) + end + end + + it 'fails for unstubbed access' do + expect(access_level_for(no_access)).to eq(Gitlab::Access::NO_ACCESS) + end + + it 'fails for invalid access level' do + expect { stub_member_access_level(object, unknown: :anything) } + .to raise_error(ArgumentError, "Invalid access level :unknown") + end + end + + context 'with project' do + let(:object) { build_stubbed(:project) } + + it_behaves_like 'access level stubs' do + def access_level_for(user) + object.team.max_member_access(user.id) + end + end + end + + context 'with group' do + let(:object) { build_stubbed(:group) } + + it_behaves_like 'access level stubs' do + def access_level_for(user) + object.max_member_access_for_user(user) + end + end + end + + context 'with unsupported object' do + let(:object) { :a_symbol } + + it 'raises an error' do + expect { stub_member_access_level(object) } + .to raise_error(ArgumentError, "Stubbing member access level unsupported for :a_symbol (Symbol)") + end + end + end +end diff --git a/spec/tooling/lib/tooling/find_changes_spec.rb b/spec/tooling/lib/tooling/find_changes_spec.rb index 5932eb5e919..37e590858cf 100644 --- a/spec/tooling/lib/tooling/find_changes_spec.rb +++ b/spec/tooling/lib/tooling/find_changes_spec.rb @@ -11,12 +11,17 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do attr_accessor :changed_files_file, :predictive_tests_file, :frontend_fixtures_mapping_file let(:instance) do - described_class.new(changed_files_pathname, predictive_tests_pathname, frontend_fixtures_mapping_pathname) + described_class.new( + changed_files_pathname: changed_files_pathname, + predictive_tests_pathname: predictive_tests_pathname, + frontend_fixtures_mapping_pathname: frontend_fixtures_mapping_pathname, + from: from) end let(:changed_files_pathname) { changed_files_file.path } let(:predictive_tests_pathname) { predictive_tests_file.path } let(:frontend_fixtures_mapping_pathname) { frontend_fixtures_mapping_file.path } + let(:from) { :api } let(:gitlab_client) { double('GitLab') } # rubocop:disable RSpec/VerifiedDoubles around do |example| @@ -45,24 +50,50 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do 'CI_MERGE_REQUEST_PROJECT_PATH' => 'dummy-project', 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'dummy-token' ) + end + + describe '#initialize' do + context 'when fetching changes from unknown' do + let(:from) { :unknown } - allow(instance).to receive(:gitlab).and_return(gitlab_client) + it 'raises an ArgumentError' do + expect { instance }.to raise_error( + ArgumentError, ":from can only be :api or :changed_files" + ) + end + end end describe '#execute' do subject { instance.execute } + before do + allow(instance).to receive(:gitlab).and_return(gitlab_client) + end + context 'when there is no changed files file' do let(:changed_files_pathname) { nil } it 'raises an ArgumentError' do expect { subject }.to raise_error( - ArgumentError, "A path to the changed files file must be given as first argument." + ArgumentError, "A path to the changed files file must be given as :changed_files_pathname" ) end end - context 'when an changed files file is provided' do + context 'when fetching changes from API' do + let(:from) { :api } + + it 'calls GitLab API to retrieve the MR diff' do + expect(gitlab_client).to receive_message_chain(:merge_request_changes, :changes).and_return([]) + + subject + end + end + + context 'when fetching changes from changed files' do + let(:from) { :changed_files } + it 'does not call GitLab API to retrieve the MR diff' do expect(gitlab_client).not_to receive(:merge_request_changes) @@ -154,68 +185,96 @@ RSpec.describe Tooling::FindChanges, feature_category: :tooling do describe '#only_js_files_changed' do subject { instance.only_js_files_changed } - let(:mr_changes_array) { [] } + context 'when fetching changes from changed files' do + let(:from) { :changed_files } - before do - # The class from the GitLab gem isn't public, so we cannot use verified doubles for it. - # - # rubocop:disable RSpec/VerifiedDoubles - allow(gitlab_client).to receive(:merge_request_changes) - .with('dummy-project', '1234') - .and_return(double(changes: mr_changes_array)) - # rubocop:enable RSpec/VerifiedDoubles - end + before do + File.write(changed_files_pathname, changed_files_file_content) + end - context 'when a file is passed as an argument' do - let(:changed_files_pathname) { 'does-not-exist.out' } + context 'when changed files contain only *.js changes' do + let(:changed_files_file_content) { 'a.js b.js' } - it 'calls GitLab API' do - expect(gitlab_client).to receive(:merge_request_changes) - .with('dummy-project', '1234') + it 'returns true' do + expect(subject).to be true + end + end - subject + context 'when changed files contain not only *.js changes' do + let(:changed_files_file_content) { 'a.js b.rb' } + + it 'returns false' do + expect(subject).to be false + end end end - context 'when there are no file changes' do + context 'when fetching changes from API' do + let(:from) { :api } + let(:mr_changes_array) { [] } - it 'returns false' do - expect(subject).to be false + before do + allow(instance).to receive(:gitlab).and_return(gitlab_client) + + # The class from the GitLab gem isn't public, so we cannot use verified doubles for it. + # + # rubocop:disable RSpec/VerifiedDoubles + allow(gitlab_client).to receive(:merge_request_changes) + .with('dummy-project', '1234') + .and_return(double(changes: mr_changes_array)) + # rubocop:enable RSpec/VerifiedDoubles end - end - context 'when there are changes to files other than JS files' do - let(:mr_changes_array) do - [ - { - "new_path" => "scripts/gitlab_component_helpers.sh", - "old_path" => "scripts/gitlab_component_helpers.sh" - }, - { - "new_path" => "scripts/test.js", - "old_path" => "scripts/test.js" - } - ] + context 'when a file is passed as an argument' do + it 'calls GitLab API' do + expect(gitlab_client).to receive(:merge_request_changes) + .with('dummy-project', '1234') + + subject + end end - it 'returns false' do - expect(subject).to be false + context 'when there are no file changes' do + let(:mr_changes_array) { [] } + + it 'returns false' do + expect(subject).to be false + end end - end - context 'when there are changes only to JS files' do - let(:mr_changes_array) do - [ - { - "new_path" => "scripts/test.js", - "old_path" => "scripts/test.js" - } - ] + context 'when there are changes to files other than JS files' do + let(:mr_changes_array) do + [ + { + "new_path" => "scripts/gitlab_component_helpers.sh", + "old_path" => "scripts/gitlab_component_helpers.sh" + }, + { + "new_path" => "scripts/test.js", + "old_path" => "scripts/test.js" + } + ] + end + + it 'returns false' do + expect(subject).to be false + end end - it 'returns true' do - expect(subject).to be true + context 'when there are changes only to JS files' do + let(:mr_changes_array) do + [ + { + "new_path" => "scripts/test.js", + "old_path" => "scripts/test.js" + } + ] + end + + it 'returns true' do + expect(subject).to be true + end end end end diff --git a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb index d6f68baeb90..b78f0a3bb6b 100644 --- a/spec/tooling/lib/tooling/helpers/file_handler_spec.rb +++ b/spec/tooling/lib/tooling/helpers/file_handler_spec.rb @@ -67,10 +67,10 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do end describe '#write_array_to_file' do - let(:content_array) { %w[new_entry] } - let(:overwrite_flag) { false } + let(:content_array) { %w[new_entry] } + let(:append_flag) { true } - subject { instance.write_array_to_file(output_file_path, content_array, overwrite: overwrite_flag) } + subject { instance.write_array_to_file(output_file_path, content_array, append: append_flag) } context 'when the output file does not exist' do let(:non_existing_output_file) { 'tmp/another_file.out' } @@ -113,8 +113,8 @@ RSpec.describe Tooling::Helpers::FileHandler, feature_category: :tooling do .to((initial_content.split(' ') + content_array).join(' ')) end - context 'when the overwrite flag is set to true' do - let(:overwrite_flag) { true } + context 'when the append flag is set to false' do + let(:append_flag) { false } it 'overwrites the previous content' do expect { subject }.to change { File.read(output_file_path) } diff --git a/spec/tooling/lib/tooling/predictive_tests_spec.rb b/spec/tooling/lib/tooling/predictive_tests_spec.rb index 79554037c48..b82364fe6f6 100644 --- a/spec/tooling/lib/tooling/predictive_tests_spec.rb +++ b/spec/tooling/lib/tooling/predictive_tests_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'tempfile' +require 'fileutils' require_relative '../../../../tooling/lib/tooling/predictive_tests' require_relative '../../../support/helpers/stub_env' @@ -9,11 +10,14 @@ RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do let(:instance) { described_class.new } let(:matching_tests_initial_content) { 'initial_matching_spec' } + let(:fixtures_mapping_content) { '{}' } - attr_accessor :changed_files, :fixtures_mapping, :matching_js_files, :matching_tests, :views_with_partials + attr_accessor :changed_files, :changed_files_path, :fixtures_mapping, + :matching_js_files, :matching_tests, :views_with_partials around do |example| self.changed_files = Tempfile.new('test-folder/changed_files.txt') + self.changed_files_path = changed_files.path self.fixtures_mapping = Tempfile.new('test-folder/fixtures_mapping.txt') self.matching_js_files = Tempfile.new('test-folder/matching_js_files.txt') self.matching_tests = Tempfile.new('test-folder/matching_tests.txt') @@ -22,10 +26,15 @@ RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/ # Tempfile.html#class-Tempfile-label-Explicit+close begin - example.run - ensure + # In practice, we let PredictiveTests create the file, and we just + # use its file name. changed_files.close changed_files.unlink + + example.run + ensure + # Since example.run can create the file again, let's remove it again + FileUtils.rm_f(changed_files_path) fixtures_mapping.close fixtures_mapping.unlink matching_js_files.close @@ -39,7 +48,7 @@ RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do before do stub_env( - 'RSPEC_CHANGED_FILES_PATH' => changed_files.path, + 'RSPEC_CHANGED_FILES_PATH' => changed_files_path, 'RSPEC_MATCHING_TESTS_PATH' => matching_tests.path, 'RSPEC_VIEWS_INCLUDING_PARTIALS_PATH' => views_with_partials.path, 'FRONTEND_FIXTURES_MAPPING_PATH' => fixtures_mapping.path, @@ -50,7 +59,9 @@ RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do # We write some data to later on verify that we only append to this file. File.write(matching_tests.path, matching_tests_initial_content) - File.write(fixtures_mapping.path, '{}') # We write valid JSON, so that the file can be processed + File.write(fixtures_mapping.path, fixtures_mapping_content) + + allow(Gitlab).to receive(:configure) end describe '#execute' do @@ -73,14 +84,18 @@ RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do context 'when all ENV variables are provided' do before do - File.write(changed_files, changed_files_content) + change = double('GitLab::Change') # rubocop:disable RSpec/VerifiedDoubles + allow(change).to receive_message_chain(:to_h, :values_at) + .and_return([changed_files_content, changed_files_content]) + + allow(Gitlab).to receive_message_chain(:merge_request_changes, :changes) + .and_return([change]) end context 'when no files were changed' do let(:changed_files_content) { '' } - it 'does not change any files' do - expect { subject }.not_to change { File.read(changed_files.path) } + it 'does not change files other than RSPEC_CHANGED_FILES_PATH' do expect { subject }.not_to change { File.read(matching_tests.path) } expect { subject }.not_to change { File.read(views_with_partials.path) } expect { subject }.not_to change { File.read(fixtures_mapping.path) } @@ -88,17 +103,27 @@ RSpec.describe Tooling::PredictiveTests, feature_category: :tooling do end end - context 'when some files were changed' do - let(:changed_files_content) { 'tooling/lib/tooling/predictive_tests.rb' } + context 'when some files used for frontend fixtures were changed' do + let(:changed_files_content) { 'app/models/todo.rb' } + let(:changed_files_matching_test) { 'spec/models/todo_spec.rb' } + let(:matching_frontend_fixture) { 'tmp/tests/frontend/fixtures-ee/todos/todos.html' } + let(:fixtures_mapping_content) do + JSON.dump(changed_files_matching_test => [matching_frontend_fixture]) # rubocop:disable Gitlab/Json + end + + it 'writes to RSPEC_CHANGED_FILES_PATH with API contents and appends with matching fixtures' do + subject + + expect(File.read(changed_files_path)).to eq("#{changed_files_content} #{matching_frontend_fixture}") + end it 'appends the spec file to RSPEC_MATCHING_TESTS_PATH' do expect { subject }.to change { File.read(matching_tests.path) } .from(matching_tests_initial_content) - .to("#{matching_tests_initial_content} spec/tooling/lib/tooling/predictive_tests_spec.rb") + .to("#{matching_tests_initial_content} #{changed_files_matching_test}") end - it 'does not change files other than RSPEC_MATCHING_TESTS_PATH' do - expect { subject }.not_to change { File.read(changed_files.path) } + it 'does not change files other than RSPEC_CHANGED_FILES_PATH nor RSPEC_MATCHING_TESTS_PATH' do expect { subject }.not_to change { File.read(views_with_partials.path) } expect { subject }.not_to change { File.read(fixtures_mapping.path) } expect { subject }.not_to change { File.read(matching_js_files.path) } diff --git a/tooling/bin/find_only_js_changes b/tooling/bin/find_only_js_changes index 67e3d3c6ff5..a69ee64fe14 100755 --- a/tooling/bin/find_only_js_changes +++ b/tooling/bin/find_only_js_changes @@ -3,7 +3,7 @@ require_relative '../lib/tooling/find_changes' -if Tooling::FindChanges.new.only_js_files_changed +if Tooling::FindChanges.new(from: :api).only_js_files_changed puts "Only JS files were changed" exit 0 else diff --git a/tooling/lib/tooling/find_changes.rb b/tooling/lib/tooling/find_changes.rb index b4439bd4abe..d8373d11245 100755 --- a/tooling/lib/tooling/find_changes.rb +++ b/tooling/lib/tooling/find_changes.rb @@ -9,8 +9,15 @@ module Tooling include Helpers::FileHandler def initialize( - changed_files_pathname = nil, predictive_tests_pathname = nil, frontend_fixtures_mapping_pathname = nil + from:, + changed_files_pathname: nil, + predictive_tests_pathname: nil, + frontend_fixtures_mapping_pathname: nil ) + + raise ArgumentError, ':from can only be :api or :changed_files' unless + %i[api changed_files].include?(from) + @gitlab_token = ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] || '' @gitlab_endpoint = ENV['CI_API_V4_URL'] @mr_project_path = ENV['CI_MERGE_REQUEST_PROJECT_PATH'] @@ -18,20 +25,23 @@ module Tooling @changed_files_pathname = changed_files_pathname @predictive_tests_pathname = predictive_tests_pathname @frontend_fixtures_mapping_pathname = frontend_fixtures_mapping_pathname + @from = from end def execute if changed_files_pathname.nil? - raise ArgumentError, "A path to the changed files file must be given as first argument." + raise ArgumentError, "A path to the changed files file must be given as :changed_files_pathname" end - add_frontend_fixture_files! - write_array_to_file(changed_files_pathname, file_changes, overwrite: true) + case @from + when :api + write_array_to_file(changed_files_pathname, file_changes + frontend_fixture_files, append: false) + else + write_array_to_file(changed_files_pathname, frontend_fixture_files, append: true) + end end def only_js_files_changed - @changed_files_pathname = nil # We ensure that we'll get the diff from the MR directly, not from a file. - file_changes.any? && file_changes.all? { |file| file.end_with?('.js') } end @@ -55,25 +65,26 @@ module Tooling predictive_tests_pathname && frontend_fixtures_mapping_pathname end - def add_frontend_fixture_files! - return unless add_frontend_fixture_files? - + def frontend_fixture_files # If we have a `test file -> JSON frontend fixture` mapping file, we add the files JSON frontend fixtures # files to the list of changed files so that Jest can automatically run the dependent tests # using --findRelatedTests flag. - test_files.each do |test_file| - file_changes.concat(frontend_fixtures_mapping[test_file]) if frontend_fixtures_mapping.key?(test_file) + empty = [].freeze + + test_files.flat_map do |test_file| + frontend_fixtures_mapping[test_file] || empty end end def file_changes @file_changes ||= - if changed_files_pathname && File.exist?(changed_files_pathname) - read_array_from_file(changed_files_pathname) - else + case @from + when :api mr_changes.changes.flat_map do |change| change.to_h.values_at('old_path', 'new_path') end.uniq + else + read_array_from_file(changed_files_pathname) end end diff --git a/tooling/lib/tooling/helpers/file_handler.rb b/tooling/lib/tooling/helpers/file_handler.rb index 2778bb1ffbc..88248e31df2 100644 --- a/tooling/lib/tooling/helpers/file_handler.rb +++ b/tooling/lib/tooling/helpers/file_handler.rb @@ -11,17 +11,17 @@ module Tooling File.read(file).split(' ') end - def write_array_to_file(file, content_array, overwrite: false) + def write_array_to_file(file, content_array, append: true) FileUtils.touch file # We sort the array to make it easier to read the output file content_array.sort! output_content = - if overwrite - content_array.join(' ') + if append + [File.read(file), *content_array].join(' ').lstrip else - (File.read(file).split(' ') + content_array).join(' ') + content_array.join(' ') end File.write(file, output_content) diff --git a/tooling/lib/tooling/predictive_tests.rb b/tooling/lib/tooling/predictive_tests.rb index 2691e1ba56d..503426b5520 100644 --- a/tooling/lib/tooling/predictive_tests.rb +++ b/tooling/lib/tooling/predictive_tests.rb @@ -32,7 +32,10 @@ module Tooling end def execute - Tooling::FindChanges.new(rspec_changed_files_path).execute + Tooling::FindChanges.new( + from: :api, + changed_files_pathname: rspec_changed_files_path + ).execute Tooling::FindTests.new(rspec_changed_files_path, rspec_matching_tests_path).execute Tooling::Mappings::PartialToViewsMappings.new( rspec_changed_files_path, rspec_views_including_partials_path).execute @@ -41,7 +44,11 @@ module Tooling Tooling::Mappings::GraphqlBaseTypeMappings.new(rspec_changed_files_path, rspec_matching_tests_path).execute Tooling::Mappings::ViewToSystemSpecsMappings.new(rspec_changed_files_path, rspec_matching_tests_path).execute Tooling::FindChanges.new( - rspec_changed_files_path, rspec_matching_tests_path, frontend_fixtures_mapping_path).execute + from: :changed_files, + changed_files_pathname: rspec_changed_files_path, + predictive_tests_pathname: rspec_matching_tests_path, + frontend_fixtures_mapping_pathname: frontend_fixtures_mapping_path + ).execute Tooling::Mappings::ViewToJsMappings.new(rspec_changed_files_path, rspec_matching_js_files_path).execute end |