diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 9fbb089d973..afe900f39a6 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1001,6 +1001,7 @@ - <<: *if-merge-request-targeting-stable-branch allow_failure: true - <<: *if-ruby3-branch + allow_failure: true - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e changes: *feature-flag-development-config-patterns when: manual diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js index 1fd87b9897c..264c2629433 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js @@ -19,9 +19,14 @@ export const I18N = { ), disallowForcePushDescription: s__('BranchRules|Force push is not allowed.'), approvalsTitle: s__('BranchRules|Approvals'), + manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'), + approvalsDescription: s__( + 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}', + ), statusChecksTitle: s__('BranchRules|Status checks'), allowedToPushHeader: s__('BranchRules|Allowed to push (%{total})'), allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'), + approvalsHeader: s__('BranchRules|Required approvals (%{total})'), noData: s__('BranchRules|No data to display'), }; @@ -33,3 +38,5 @@ export const WILDCARDS_HELP_PATH = 'user/project/protected_branches#configure-multiple-protected-branches-by-using-a-wildcard'; export const PROTECTED_BRANCHES_HELP_PATH = 'user/project/protected_branches'; + +export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index.md'; diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index 6534ff883a6..318940478a8 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -11,16 +11,19 @@ import { BRANCH_PARAM_NAME, WILDCARDS_HELP_PATH, PROTECTED_BRANCHES_HELP_PATH, + APPROVALS_HELP_PATH, } from './constants'; const wildcardsHelpDocLink = helpPagePath(WILDCARDS_HELP_PATH); const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH); +const approvalsHelpDocLink = helpPagePath(APPROVALS_HELP_PATH); export default { name: 'RuleView', i18n: I18N, wildcardsHelpDocLink, protectedBranchesHelpDocLink, + approvalsHelpDocLink, components: { Protection, GlSprintf, GlLink, GlLoadingIcon }, inject: { projectPath: { @@ -29,6 +32,9 @@ export default { protectedBranchesPath: { default: '', }, + approvalRulesPath: { + default: '', + }, }, apollo: { project: { @@ -48,7 +54,9 @@ export default { data() { return { branch: getParameterByName(BRANCH_PARAM_NAME), - branchProtection: {}, + branchProtection: { + approvalRules: {}, + }, }; }, computed: { @@ -75,6 +83,15 @@ export default { total: this.pushAccessLevels.total, }); }, + approvalsHeader() { + const total = this.approvals.reduce( + (sum, { approvalsRequired }) => sum + approvalsRequired, + 0, + ); + return sprintf(this.$options.i18n.approvalsHeader, { + total, + }); + }, allBranches() { return this.branch === ALL_BRANCHES_WILDCARD; }, @@ -86,6 +103,9 @@ export default { ? this.$options.i18n.targetBranch : this.$options.i18n.branchNameOrPattern; }, + approvals() { + return this.branchProtection?.approvalRules?.nodes || []; + }, }, methods: { getAccessLevels(accessLevels = {}) { @@ -164,7 +184,22 @@ export default { /> - +

{{ $options.i18n.approvalsTitle }}

+ + + + + diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue index 8434b7bfce5..cfe2df0dbda 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue @@ -41,6 +41,11 @@ export default { required: false, default: () => [], }, + approvals: { + type: Array, + required: false, + default: () => [], + }, }, computed: { showUsersDivider() { @@ -80,5 +85,15 @@ export default { :title="$options.i18n.groupsTitle" :access-levels="groups" /> + + + diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue index 2509c2538b2..28a1c09fa82 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue @@ -36,6 +36,11 @@ export default { required: false, default: () => [], }, + approvalsRequired: { + type: Number, + required: false, + default: 0, + }, }, computed: { avatarBadgeSrOnlyText() { @@ -48,6 +53,11 @@ export default { commaSeparateList() { return this.accessLevels.length > 1; }, + approvalsRequiredTitle() { + return this.approvalsRequired + ? n__('%d approval required', '%d approvals required', this.approvalsRequired) + : null; + }, }, }; @@ -57,34 +67,44 @@ export default { class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4" :class="{ 'gl-border-t-solid': showDivider }" > -
{{ title }}
+
+
{{ title }}
- - - + + + -
- , - {{ item.accessLevelDescription }} +
+ , + {{ item.accessLevelDescription }} +
+ +
{{ approvalsRequiredTitle }}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js index 39164063d05..07fd0a7080f 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js +++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js @@ -14,7 +14,7 @@ export default function mountBranchRules(el) { defaultClient: createDefaultClient(), }); - const { projectPath, protectedBranchesPath } = el.dataset; + const { projectPath, protectedBranchesPath, approvalRulesPath } = el.dataset; return new Vue({ el, @@ -22,6 +22,7 @@ export default function mountBranchRules(el) { provide: { projectPath, protectedBranchesPath, + approvalRulesPath, }, render(h) { return h(View); diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index e34c51bc11f..ad061dd2e6b 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -85,7 +85,7 @@ export default { return this.loading || this.$apollo.queries.issuable.loading; }, canUpdate() { - return this.issuable.userPermissions?.updateMergeRequest || false; + return this.issuable.userPermissions?.adminMergeRequest || false; }, }, created() { diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index b61996cdcdb..e6c29e24f0c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -53,6 +53,11 @@ export default { required: false, default: false, }, + allowMultipleScopedLabels: { + type: Boolean, + required: false, + default: false, + }, variant: { type: String, required: false, @@ -164,6 +169,7 @@ export default { allowLabelCreate: this.allowLabelCreate, allowMultiselect: this.allowMultiselect, allowScopedLabels: this.allowScopedLabels, + allowMultipleScopedLabels: this.allowMultipleScopedLabels, dropdownButtonText: this.dropdownButtonText, selectedLabels: this.selectedLabels, labelsFetchPath: this.labelsFetchPath, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js index 43b23994cdf..c85d9befcbb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js @@ -94,14 +94,13 @@ export default { candidateLabel.indeterminate = false; } - if (isScopedLabel(candidateLabel)) { + if (isScopedLabel(candidateLabel) && !state.allowMultipleScopedLabels) { const currentActiveScopedLabel = state.labels.find( ({ set, title }) => set && title !== candidateLabel.title && scopedLabelKey({ title }) === scopedLabelKey(candidateLabel), ); - if (currentActiveScopedLabel) { currentActiveScopedLabel.set = false; } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql index 05de680ab05..f087ca6c982 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql @@ -19,7 +19,7 @@ query mergeRequestReviewers($fullPath: ID!, $iid: String!) { } } userPermissions { - updateMergeRequest + adminMergeRequest } } } diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 282622c792e..e05a32ccb5b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -455,8 +455,7 @@ module Ci def prevent_rollback_deployment? strong_memoize(:prevent_rollback_deployment) do - Feature.enabled?(:prevent_outdated_deployment_jobs, project) && - starts_environment? && + starts_environment? && project.ci_forward_deployment_enabled? && deployment&.older_than_last_successful_deployment? end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index dafcbc593be..4d10499f48d 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -105,6 +105,7 @@ class Deployment < ApplicationRecord after_transition any => :running do |deployment| next unless deployment.project.ci_forward_deployment_enabled? + next if Feature.enabled?(:prevent_outdated_deployment_jobs, deployment.project) deployment.run_after_commit do Deployments::DropOlderDeploymentsWorker.perform_async(id) diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb index 72f5ab11060..cd7d4fc409a 100644 --- a/app/models/namespace/aggregation_schedule.rb +++ b/app/models/namespace/aggregation_schedule.rb @@ -14,9 +14,9 @@ class Namespace::AggregationSchedule < ApplicationRecord def self.default_lease_timeout if Feature.enabled?(:remove_namespace_aggregator_delay) - 1.hour.to_i + 30.minutes.to_i else - 1.5.hours.to_i + 1.hour.to_i end end diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb index e6ec65fcc91..22cd267806d 100644 --- a/app/services/ci/process_build_service.rb +++ b/app/services/ci/process_build_service.rb @@ -25,6 +25,8 @@ module Ci end def enqueue(build) + return build.drop!(:failed_outdated_deployment_job) if build.prevent_rollback_deployment? + build.enqueue end diff --git a/app/views/projects/settings/branch_rules/index.html.haml b/app/views/projects/settings/branch_rules/index.html.haml index ab692a23e44..a7e80101a88 100644 --- a/app/views/projects/settings/branch_rules/index.html.haml +++ b/app/views/projects/settings/branch_rules/index.html.haml @@ -3,4 +3,4 @@ %h3.gl-mb-5= s_('BranchRules|Branch rules details') -#js-branch-rules{ data: { project_path: @project.full_path, protected_branches_path: project_settings_repository_path(@project, anchor: 'js-protected-branches-settings') } } +#js-branch-rules{ data: { project_path: @project.full_path, protected_branches_path: project_settings_repository_path(@project, anchor: 'js-protected-branches-settings'), approval_rules_path: project_settings_merge_requests_path(@project, anchor: 'js-merge-request-approval-settings') } } diff --git a/config/metrics/counts_28d/20220222215951_xmau_plan.yml b/config/metrics/counts_28d/20220222215951_xmau_plan.yml index 587f34e90c3..c254ad942c2 100644 --- a/config/metrics/counts_28d/20220222215951_xmau_plan.yml +++ b/config/metrics/counts_28d/20220222215951_xmau_plan.yml @@ -21,6 +21,7 @@ options: - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels + - users_updating_work_item_iteration data_category: optional distribution: - ce diff --git a/config/metrics/counts_28d/20220222215952_xmau_project_management.yml b/config/metrics/counts_28d/20220222215952_xmau_project_management.yml index 542f8a13118..0dad4fd0979 100644 --- a/config/metrics/counts_28d/20220222215952_xmau_project_management.yml +++ b/config/metrics/counts_28d/20220222215952_xmau_project_management.yml @@ -21,6 +21,7 @@ options: - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels + - users_updating_work_item_iteration data_category: optional distribution: - ce diff --git a/config/metrics/counts_28d/20220222215955_users_work_items.yml b/config/metrics/counts_28d/20220222215955_users_work_items.yml index e7a95c9d335..ec07fb25f11 100644 --- a/config/metrics/counts_28d/20220222215955_users_work_items.yml +++ b/config/metrics/counts_28d/20220222215955_users_work_items.yml @@ -21,6 +21,7 @@ options: - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels + - users_updating_work_item_iteration data_category: optional distribution: - ce diff --git a/config/metrics/counts_28d/20220922042106_users_updating_work_item_iteration_monthly.yml b/config/metrics/counts_28d/20220922042106_users_updating_work_item_iteration_monthly.yml new file mode 100644 index 00000000000..4c19e4e3261 --- /dev/null +++ b/config/metrics/counts_28d/20220922042106_users_updating_work_item_iteration_monthly.yml @@ -0,0 +1,24 @@ +--- +key_path: redis_hll_counters.work_items.users_updating_work_item_iteration_monthly +description: Unique users updating a work item's iteration +product_section: team planning +product_stage: dev +product_group: plan +product_category: project_management +value_type: number +status: active +milestone: "15.5" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98539 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - users_updating_work_item_iteration +distribution: +- ce +- ee +tier: +- premium +- ultimate diff --git a/config/metrics/counts_7d/20220222215851_xmau_plan.yml b/config/metrics/counts_7d/20220222215851_xmau_plan.yml index 4fb302d67ac..77325a205ee 100644 --- a/config/metrics/counts_7d/20220222215851_xmau_plan.yml +++ b/config/metrics/counts_7d/20220222215851_xmau_plan.yml @@ -21,6 +21,7 @@ options: - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels + - users_updating_work_item_iteration data_category: optional distribution: - ce diff --git a/config/metrics/counts_7d/20220222215852_xmau_project_management.yml b/config/metrics/counts_7d/20220222215852_xmau_project_management.yml index a63adfab8aa..c7e712cf92a 100644 --- a/config/metrics/counts_7d/20220222215852_xmau_project_management.yml +++ b/config/metrics/counts_7d/20220222215852_xmau_project_management.yml @@ -21,6 +21,7 @@ options: - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels + - users_updating_work_item_iteration data_category: optional distribution: - ce diff --git a/config/metrics/counts_7d/20220222215855_users_work_items.yml b/config/metrics/counts_7d/20220222215855_users_work_items.yml index cb312022192..0985f38c83b 100644 --- a/config/metrics/counts_7d/20220222215855_users_work_items.yml +++ b/config/metrics/counts_7d/20220222215855_users_work_items.yml @@ -21,6 +21,7 @@ options: - users_updating_work_item_title - users_updating_work_item_dates - users_updating_work_item_labels + - users_updating_work_item_iteration data_category: optional distribution: - ce diff --git a/config/metrics/counts_7d/20220922042528_users_updating_work_item_iteration_weekly.yml b/config/metrics/counts_7d/20220922042528_users_updating_work_item_iteration_weekly.yml new file mode 100644 index 00000000000..aad949867cf --- /dev/null +++ b/config/metrics/counts_7d/20220922042528_users_updating_work_item_iteration_weekly.yml @@ -0,0 +1,24 @@ +--- +key_path: redis_hll_counters.work_items.users_updating_work_item_iteration_weekly +description: Unique users updating a work item's iteration +product_section: team planning +product_stage: dev +product_group: plan +product_category: project_management +value_type: number +status: active +milestone: "15.5" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98539 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - users_updating_work_item_iteration +distribution: +- ce +- ee +tier: +- premium +- ultimate diff --git a/db/migrate/20221014031033_add_temp_index_to_project_features_where_releases_access_level_gt_repository.rb b/db/migrate/20221014031033_add_temp_index_to_project_features_where_releases_access_level_gt_repository.rb new file mode 100644 index 00000000000..14077e30780 --- /dev/null +++ b/db/migrate/20221014031033_add_temp_index_to_project_features_where_releases_access_level_gt_repository.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTempIndexToProjectFeaturesWhereReleasesAccessLevelGtRepository < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'tmp_idx_project_features_on_releases_al_and_repo_al_partial' + + # Temporary index to be removed in 15.6 https://gitlab.com/gitlab-org/gitlab/-/issues/377915 + def up + add_concurrent_index :project_features, + [:releases_access_level, :repository_access_level], + name: INDEX_NAME, + where: 'releases_access_level > repository_access_level' + end + + def down + remove_concurrent_index_by_name :project_features, INDEX_NAME + end +end diff --git a/db/migrate/20221014034338_populate_releases_access_level_from_repository.rb b/db/migrate/20221014034338_populate_releases_access_level_from_repository.rb new file mode 100644 index 00000000000..6e61d972bf6 --- /dev/null +++ b/db/migrate/20221014034338_populate_releases_access_level_from_repository.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class PopulateReleasesAccessLevelFromRepository < Gitlab::Database::Migration[2.0] + restrict_gitlab_migration gitlab_schema: :gitlab_main + + disable_ddl_transaction! + + def up + update_column_in_batches( + :project_features, + :releases_access_level, + Arel.sql('repository_access_level') + ) do |table, query| + query.where(table[:releases_access_level].gt(table[:repository_access_level])) + end + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20221014031033 b/db/schema_migrations/20221014031033 new file mode 100644 index 00000000000..6a24a2027c7 --- /dev/null +++ b/db/schema_migrations/20221014031033 @@ -0,0 +1 @@ +bc05939dc672c078161cd9b7dbd7f92601edb6888a77c62adb014964e30c6ae8 \ No newline at end of file diff --git a/db/schema_migrations/20221014034338 b/db/schema_migrations/20221014034338 new file mode 100644 index 00000000000..c90dfebb72b --- /dev/null +++ b/db/schema_migrations/20221014034338 @@ -0,0 +1 @@ +58ee7f51a0da4ee4ec471d4492106d1fc3124419ba83591913967d6bd38105e5 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e95f93982fa..b8779dd9416 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -30993,6 +30993,8 @@ CREATE UNIQUE INDEX taggings_idx ON taggings USING btree (tag_id, taggable_id, t CREATE UNIQUE INDEX term_agreements_unique_index ON term_agreements USING btree (user_id, term_id); +CREATE INDEX tmp_idx_project_features_on_releases_al_and_repo_al_partial ON project_features USING btree (releases_access_level, repository_access_level) WHERE (releases_access_level > repository_access_level); + CREATE INDEX tmp_idx_vulnerabilities_on_id_where_report_type_7_99 ON vulnerabilities USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99])); CREATE INDEX tmp_index_approval_merge_request_rules_on_report_type_equal_one ON approval_merge_request_rules USING btree (id, report_type) WHERE (report_type = 1); diff --git a/doc/ci/pipelines/cicd_minutes.md b/doc/ci/pipelines/cicd_minutes.md index 14a8ad7f59a..a5946607128 100644 --- a/doc/ci/pipelines/cicd_minutes.md +++ b/doc/ci/pipelines/cicd_minutes.md @@ -248,7 +248,6 @@ GitLab SaaS runners have different cost factors, depending on the runner type (L | Linux OS + Docker executor| Small |1| | Linux OS + Docker executor| Medium |2| | Linux OS + Docker executor| Large |3| -| macOS + shell executor | Large| 6 | ### Monthly reset of CI/CD minutes diff --git a/doc/ci/pipelines/schedules.md b/doc/ci/pipelines/schedules.md index d9f4cf3aaef..0eeb0eada87 100644 --- a/doc/ci/pipelines/schedules.md +++ b/doc/ci/pipelines/schedules.md @@ -39,6 +39,9 @@ To add a pipeline schedule: These variables are available only when the scheduled pipeline runs, and not in any other pipeline run. +If the project already has the [maximum number of pipeline schedules](../../administration/instance_limits.md#number-of-pipeline-schedules), +you must delete unused schedules before you can add another. + ## Edit a pipeline schedule > Introduced in GitLab 14.8, only a pipeline schedule owner can edit the schedule. diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index bc362b07385..2f23ebbfd9f 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -95,9 +95,14 @@ after a given period of time. The following are example projects that demonstrate Review App configuration: -- [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx). -- [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift). -- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad). +| Project | Configuration file | +|-------------------------------------------------------------------------|--------------------| +| [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx) | [`.gitlab-ci.yml`](https://gitlab.com/gitlab-examples/review-apps-nginx/-/blob/b9c1f6a8a7a0dfd9c8784cbf233c0a7b6a28ff27/.gitlab-ci.yml#L20) | +| [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift) | [`.gitlab-ci.yml`](https://gitlab.com/gitlab-examples/review-apps-openshift/-/blob/82ebd572334793deef2d5ddc379f38942f3488be/.gitlab-ci.yml#L42) | +| [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad) | [`.gitlab-ci.yml`](https://gitlab.com/gitlab-examples/review-apps-nomad/-/blob/ca372c778be7aaed5e82d3be24e98c3f10a465af/.gitlab-ci.yml#L110) | +| [GitLab Documentation](https://gitlab.com/gitlab-org/gitlab-docs/) | [`build-and-deploy.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/a715625496303cbd90ff89f3d3658ea8d36ce0f3/.gitlab/ci/build-and-deploy.gitlab-ci.yml#L59) | +| [`https://about.gitlab.com/`](https://gitlab.com/gitlab-com/www-gitlab-com/) | [`.gitlab-ci.yml`](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/6ffcdc3cb9af2abed490cbe5b7417df3e83cd76c/.gitlab-ci.yml#L332) | +| [GitLab Insights](https://gitlab.com/gitlab-org/gitlab-insights/) | [`.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab-insights/-/blob/9e63f44ac2a5a4defc965d0d61d411a768e20546/.gitlab-ci.yml#L234) | Other examples of Review Apps: diff --git a/lib/gitlab/usage_data_counters/known_events/work_items.yml b/lib/gitlab/usage_data_counters/known_events/work_items.yml index 94b7a37e67e..ee828fc0f72 100644 --- a/lib/gitlab/usage_data_counters/known_events/work_items.yml +++ b/lib/gitlab/usage_data_counters/known_events/work_items.yml @@ -19,3 +19,11 @@ redis_slot: users aggregation: weekly feature_flag: track_work_items_activity +- name: users_updating_work_item_iteration + # The event tracks an EE feature. + # It's added here so it can be aggregated into the CE/EE 'OR' aggregate metrics. + # It will report 0 for CE instances and should not be used with 'AND' aggregators. + category: work_items + redis_slot: users + aggregation: weekly + feature_flag: track_work_items_activity diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 915c3271ff9..34cdc9c4553 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -143,6 +143,11 @@ msgid_plural "%d additional users" msgstr[0] "" msgstr[1] "" +msgid "%d approval required" +msgid_plural "%d approvals required" +msgstr[0] "" +msgstr[1] "" + msgid "%d approver" msgid_plural "%d approvers" msgstr[0] "" @@ -6826,6 +6831,9 @@ msgstr "" msgid "BranchRules|Approvals" msgstr "" +msgid "BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}" +msgstr "" + msgid "BranchRules|Branch" msgstr "" @@ -6853,6 +6861,9 @@ msgstr "" msgid "BranchRules|Keep stable branches secure and force developers to use merge requests. %{linkStart}What are protected branches?%{linkEnd}" msgstr "" +msgid "BranchRules|Manage in Merge Request Approvals" +msgstr "" + msgid "BranchRules|Manage in Protected Branches" msgstr "" @@ -6874,6 +6885,9 @@ msgstr "" msgid "BranchRules|Require approval from code owners." msgstr "" +msgid "BranchRules|Required approvals (%{total})" +msgstr "" + msgid "BranchRules|Roles" msgstr "" @@ -9841,6 +9855,9 @@ msgstr "" msgid "Complete verification to sign up." msgstr "" +msgid "Complete with errors" +msgstr "" + msgid "Completed" msgstr "" @@ -12572,6 +12589,9 @@ msgstr "" msgid "Decrease" msgstr "" +msgid "Default - Never run" +msgstr "" + msgid "Default CI/CD configuration file" msgstr "" @@ -21985,6 +22005,9 @@ msgstr "" msgid "Invalid yaml" msgstr "" +msgid "Invalidated" +msgstr "" + msgid "Investigate vulnerability: %{title}" msgstr "" @@ -30441,6 +30464,27 @@ msgstr "" msgid "Pre-defined push rules" msgstr "" +msgid "PreScanVerification|(optional)" +msgstr "" + +msgid "PreScanVerification|Last run %{timeAgo} in pipeline" +msgstr "" + +msgid "PreScanVerification|Pre-scan verification" +msgstr "" + +msgid "PreScanVerification|Started %{timeAgo} in pipeline" +msgstr "" + +msgid "PreScanVerification|Test your configuration and identify potential errors before running a full scan." +msgstr "" + +msgid "PreScanVerification|Verify configuration" +msgstr "" + +msgid "PreScanVerification|View results" +msgstr "" + msgid "Preferences" msgstr "" diff --git a/qa/Gemfile b/qa/Gemfile index 5817dcd2f90..01a32a7bfb2 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -16,9 +16,9 @@ gem 'rspec-retry', '~> 0.6.1', require: 'rspec/retry' gem 'rspec_junit_formatter', '~> 0.4.1' gem 'faker', '~> 2.19', '>= 2.19.0' gem 'knapsack', '~> 4.0' -gem 'parallel_tests', '~> 2.29' +gem 'parallel_tests', '~> 2.32' gem 'rotp', '~> 3.1.0' -gem 'timecop', '~> 0.9.1' +gem 'timecop', '~> 0.9.5' gem 'parallel', '~> 1.19' gem 'rainbow', '~> 3.0.0' gem 'rspec-parameterized', '~> 0.4.2' @@ -45,5 +45,5 @@ gem 'deprecation_toolkit', '~> 1.5.1', require: false group :development do gem 'pry-byebug', '~> 3.5.1', platform: :mri - gem "ruby-debug-ide", "~> 0.7.0" + gem "ruby-debug-ide", "~> 0.7.3" end diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 9917df25a65..ec5d7063182 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -196,7 +196,7 @@ GEM oj (3.13.11) os (1.1.4) parallel (1.19.2) - parallel_tests (2.29.0) + parallel_tests (2.32.0) parallel parser (3.1.2.1) ast (~> 2.4.1) @@ -255,7 +255,7 @@ GEM rspec-support (3.10.2) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) - ruby-debug-ide (0.7.2) + ruby-debug-ide (0.7.3) rake (>= 0.8.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -277,7 +277,7 @@ GEM terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) thread_safe (0.3.6) - timecop (0.9.1) + timecop (0.9.5) trailblazer-option (0.1.2) tzinfo (2.0.5) concurrent-ruby (~> 1.0) @@ -332,7 +332,7 @@ DEPENDENCIES nokogiri (~> 1.13, >= 1.13.8) octokit (~> 5.6.1) parallel (~> 1.19) - parallel_tests (~> 2.29) + parallel_tests (~> 2.32) pry-byebug (~> 3.5.1) rainbow (~> 3.0.0) rake (~> 13) @@ -342,11 +342,11 @@ DEPENDENCIES rspec-parameterized (~> 0.4.2) rspec-retry (~> 0.6.1) rspec_junit_formatter (~> 0.4.1) - ruby-debug-ide (~> 0.7.0) + ruby-debug-ide (~> 0.7.3) selenium-webdriver (~> 4.0) slack-notifier (~> 2.4) terminal-table (~> 3.0.0) - timecop (~> 0.9.1) + timecop (~> 0.9.5) warning (~> 1.3) webdrivers (~> 5.2) zeitwerk (~> 2.4) diff --git a/scripts/api/pipeline_failed_jobs.rb b/scripts/api/pipeline_failed_jobs.rb index 3c29e8842d3..c25567af698 100644 --- a/scripts/api/pipeline_failed_jobs.rb +++ b/scripts/api/pipeline_failed_jobs.rb @@ -31,6 +31,13 @@ class PipelineFailedJobs failed_jobs << job end + client.pipeline_bridges(project, pipeline_id, scope: 'failed', per_page: 100).auto_paginate do |job| + next if exclude_allowed_to_fail_jobs && job.allow_failure + + job.web_url = job.downstream_pipeline.web_url # job.web_url is linking to an invalid page + failed_jobs << job + end + failed_jobs end diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index 3176b28d547..bf4026b65db 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -33,6 +33,7 @@ describe('View branch rules', () => { let fakeApollo; const projectPath = 'test/testing'; const protectedBranchesPath = 'protected/branches'; + const approvalRulesPath = 'approval/rules'; const branchProtectionsMockRequestHandler = jest .fn() .mockResolvedValue(branchProtectionsMockResponse); @@ -42,7 +43,7 @@ describe('View branch rules', () => { wrapper = shallowMountExtended(RuleView, { apolloProvider: fakeApollo, - provide: { projectPath, protectedBranchesPath }, + provide: { projectPath, protectedBranchesPath, approvalRulesPath }, }); await waitForPromises(); @@ -57,6 +58,7 @@ describe('View branch rules', () => { const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle); const findBranchProtections = () => wrapper.findAllComponents(Protection); const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription); + const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle); it('gets the branch param from url and renders it in the view', () => { expect(util.getParameterByName).toHaveBeenCalledWith('branch'); @@ -98,4 +100,14 @@ describe('View branch rules', () => { ...protectionMockProps, }); }); + + it('renders a branch protection component for approvals', () => { + expect(findApprovalsTitle().exists()).toBe(true); + + expect(findBranchProtections().at(2).props()).toMatchObject({ + header: sprintf(I18N.approvalsHeader, { total: 0 }), + headerLinkHref: approvalRulesPath, + headerLinkTitle: I18N.manageApprovalsLinkTitle, + }); + }); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js index c5774977205..c3f573061da 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js @@ -36,6 +36,8 @@ const accessLevelsMock = [ { accessLevelDescription: 'Maintainer' }, ]; +const approvalsRequired = 3; + const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }]; export const protectionPropsMock = { @@ -45,12 +47,20 @@ export const protectionPropsMock = { roles: accessLevelsMock, users: usersMock, groups: groupsMock, + approvals: [ + { + name: 'test', + eligibleApprovers: { nodes: usersMock }, + approvalsRequired, + }, + ], }; export const protectionRowPropsMock = { title: 'Test title', users: usersMock, accessLevels: accessLevelsMock, + approvalsRequired, }; export const accessLevelsMockResponse = [ diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js index 7770e1fb2aa..b0a69bedd3e 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js @@ -25,6 +25,8 @@ describe('Branch rule protection row', () => { const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink); const findAvatars = () => wrapper.findAllComponents(GlAvatar); const findAccessLevels = () => wrapper.findAllByTestId('access-level'); + const findApprovalsRequired = () => + wrapper.findByText(`${protectionRowPropsMock.approvalsRequired} approvals required`); it('renders a title', () => { expect(findTitle().exists()).toBe(true); @@ -62,4 +64,8 @@ describe('Branch rule protection row', () => { protectionRowPropsMock.accessLevels[1].accessLevelDescription, ); }); + + it('renders the number of approvals required', () => { + expect(findApprovalsRequired().exists()).toBe(true); + }); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js index 91d16fd86a6..e2fbb4f5bbb 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js @@ -56,4 +56,13 @@ describe('Branch rule protection', () => { title: i18n.groupsTitle, }); }); + + it('renders a protection row for approvals', () => { + const approval = protectionPropsMock.approvals[0]; + expect(findProtectionRows().at(3).props()).toMatchObject({ + title: approval.name, + users: approval.eligibleApprovers.nodes, + approvalsRequired: approval.approvalsRequired, + }); + }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js index 99034e360a1..2b2508b5e11 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js @@ -193,6 +193,16 @@ describe('LabelsSelect Mutations', () => { expect(state.labels[l.id - 1].set).toBe(false); }); }); + it('allows selection of multiple scoped labels', () => { + const state = { labels: cloneDeep(labels), allowMultipleScopedLabels: true }; + + mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: labels[4].id }] }); + mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: labels[5].id }] }); + + expect(state.labels[4].set).toBe(true); + expect(state.labels[5].set).toBe(true); + expect(state.labels[6].set).toBe(true); + }); }); describe(`${types.UPDATE_LABELS_SET_STATE}`, () => { diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb index c13a5ba4d72..3e315692d0a 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggreg let(:metric_definition) do { data_source: 'redis_hll', - time_frame: '7d', + time_frame: time_frame, options: { aggregate: { operator: 'OR' @@ -15,6 +15,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggreg users_creating_work_items users_updating_work_item_title users_updating_work_item_dates + users_updating_work_item_iteration ] } } @@ -24,31 +25,36 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggreg freeze_time { example.run } end - describe '#available?' do - it 'returns false without track_work_items_activity feature' do - stub_feature_flags(track_work_items_activity: false) + where(:time_frame) { [['28d'], ['7d']] } - expect(described_class.new(metric_definition).available?).to eq(false) + with_them do + describe '#available?' do + it 'returns false without track_work_items_activity feature' do + stub_feature_flags(track_work_items_activity: false) + + expect(described_class.new(metric_definition).available?).to eq(false) + end + + it 'returns true with track_work_items_activity feature' do + stub_feature_flags(track_work_items_activity: true) + + expect(described_class.new(metric_definition).available?).to eq(true) + end end - it 'returns true with track_work_items_activity feature' do - stub_feature_flags(track_work_items_activity: true) + describe '#value', :clean_gitlab_redis_shared_state do + let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter } - expect(described_class.new(metric_definition).available?).to eq(true) - end - end + before do + counter.track_event(:users_creating_work_items, values: 1, time: 1.week.ago) + counter.track_event(:users_updating_work_item_title, values: 1, time: 1.week.ago) + counter.track_event(:users_updating_work_item_dates, values: 2, time: 1.week.ago) + counter.track_event(:users_updating_work_item_iteration, values: 2, time: 1.week.ago) + end - describe '#value', :clean_gitlab_redis_shared_state do - let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter } - - before do - counter.track_event(:users_creating_work_items, values: 1, time: 1.week.ago) - counter.track_event(:users_updating_work_item_title, values: 1, time: 1.week.ago) - counter.track_event(:users_updating_work_item_dates, values: 2, time: 1.week.ago) - end - - it 'has correct value' do - expect(described_class.new(metric_definition).value).to eq 2 + it 'has correct value' do + expect(described_class.new(metric_definition).value).to eq 2 + end end end end diff --git a/spec/migrations/populate_releases_access_level_from_repository_spec.rb b/spec/migrations/populate_releases_access_level_from_repository_spec.rb new file mode 100644 index 00000000000..2bb97662923 --- /dev/null +++ b/spec/migrations/populate_releases_access_level_from_repository_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe PopulateReleasesAccessLevelFromRepository, :migration do + let(:projects) { table(:projects) } + let(:groups) { table(:namespaces) } + let(:project_features) { table(:project_features) } + + let(:group) { groups.create!(name: 'test-group', path: 'test-group') } + let(:project) { projects.create!(namespace_id: group.id, project_namespace_id: group.id) } + let(:project_feature) do + project_features.create!(project_id: project.id, pages_access_level: 20, **project_feature_attributes) + end + + # repository_access_level and releases_access_level default to ENABLED + describe '#up' do + context 'when releases_access_level is greater than repository_access_level' do + let(:project_feature_attributes) { { repository_access_level: ProjectFeature::PRIVATE } } + + it 'reduces releases_access_level to match repository_access_level' do + expect { migrate! }.to change { project_feature.reload.releases_access_level } + .from(ProjectFeature::ENABLED) + .to(ProjectFeature::PRIVATE) + end + end + + context 'when releases_access_level is less than repository_access_level' do + let(:project_feature_attributes) { { releases_access_level: ProjectFeature::DISABLED } } + + it 'does not change releases_access_level' do + expect { migrate! }.not_to change { project_feature.reload.releases_access_level } + .from(ProjectFeature::DISABLED) + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index ec0fe456d99..9713734e97a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -632,15 +632,6 @@ RSpec.describe Ci::Build do it { expect(subject).to be_falsey } end - context 'when prevent_outdated_deployment_jobs FF is disabled' do - before do - stub_feature_flags(prevent_outdated_deployment_jobs: false) - expect(build.deployment).not_to receive(:rollback?) - end - - it { expect(subject).to be_falsey } - end - context 'when build can prevent rollback deployment' do before do expect(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true) diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 87fa5289795..bf1cf9856a0 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -171,11 +171,22 @@ RSpec.describe Deployment do end it 'executes Deployments::DropOlderDeploymentsWorker asynchronously' do + stub_feature_flags(prevent_outdated_deployment_jobs: false) + expect(Deployments::DropOlderDeploymentsWorker) .to receive(:perform_async).once.with(deployment.id) deployment.run! end + + it 'does not execute Deployments::DropOlderDeploymentsWorker when FF enabled' do + stub_feature_flags(prevent_outdated_deployment_jobs: true) + + expect(Deployments::DropOlderDeploymentsWorker) + .not_to receive(:perform_async).with(deployment.id) + + deployment.run! + end end context 'when deployment succeeded' do diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb index 3f6a890654a..45b66fa12dd 100644 --- a/spec/models/namespace/aggregation_schedule_spec.rb +++ b/spec/models/namespace/aggregation_schedule_spec.rb @@ -12,14 +12,14 @@ RSpec.describe Namespace::AggregationSchedule, :clean_gitlab_redis_shared_state, describe "#default_lease_timeout" do subject(:default_lease_timeout) { default_timeout } - it { is_expected.to eq 1.hour.to_i } + it { is_expected.to eq 30.minutes.to_i } context 'when remove_namespace_aggregator_delay FF is disabled' do before do stub_feature_flags(remove_namespace_aggregator_delay: false) end - it { is_expected.to eq 1.5.hours.to_i } + it { is_expected.to eq 1.hour.to_i } end end diff --git a/workhorse/go.mod b/workhorse/go.mod index d2d5e4b44f9..0843fe48130 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -34,7 +34,7 @@ require ( golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 golang.org/x/net v0.0.0-20220722155237-a158d28d115b golang.org/x/tools v0.1.12 - google.golang.org/grpc v1.50.0 + google.golang.org/grpc v1.50.1 google.golang.org/protobuf v1.28.1 honnef.co/go/tools v0.3.3 ) diff --git a/workhorse/go.sum b/workhorse/go.sum index 3795e8aae7a..9515f7d0384 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -1550,8 +1550,8 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0 h1:fPVVDxY9w++VjTZsYvXWqEf9Rqar/e+9zYfxKK+W+YU= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=