Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-18 18:09:22 +00:00
parent b556d0fab7
commit e4220eecca
73 changed files with 918 additions and 293 deletions

View File

@ -29,7 +29,7 @@ cache-workhorse:
variables: variables:
WEBPACK_REPORT: "false" WEBPACK_REPORT: "false"
script: script:
- !reference [.yarn-install, script] - yarn_install_script
- export GITLAB_ASSETS_HASH=$(bundle exec rake gitlab:assets:hash_sum) - export GITLAB_ASSETS_HASH=$(bundle exec rake gitlab:assets:hash_sum)
- source scripts/gitlab_component_helpers.sh - source scripts/gitlab_component_helpers.sh
- 'gitlab_assets_archive_doesnt_exist || { echoinfo "INFO: Exiting early as package exists."; exit 0; }' - 'gitlab_assets_archive_doesnt_exist || { echoinfo "INFO: Exiting early as package exists."; exit 0; }'

View File

@ -1,13 +1,3 @@
.yarn-install:
script:
- source scripts/utils.sh
- yarn_install_script
.storybook-yarn-install:
script:
- source scripts/utils.sh
- run_timed_command "retry yarn run storybook:install --frozen-lockfile"
.compile-assets-base: .compile-assets-base:
extends: extends:
- .default-retry - .default-retry
@ -98,20 +88,22 @@ update-assets-compile-test-cache:
update-yarn-cache: update-yarn-cache:
extends: extends:
- .default-retry - .default-retry
- .default-utils-before_script
- .yarn-cache-push - .yarn-cache-push
- .shared:rules:update-cache - .shared:rules:update-cache
stage: prepare stage: prepare
script: script:
- !reference [.yarn-install, script] - yarn_install_script
update-storybook-yarn-cache: update-storybook-yarn-cache:
extends: extends:
- .default-retry - .default-retry
- .default-utils-before_script
- .storybook-yarn-cache-push - .storybook-yarn-cache-push
- .shared:rules:update-cache - .shared:rules:update-cache
stage: prepare stage: prepare
script: script:
- !reference [.storybook-yarn-install, script] - yarn_install_script
.frontend-fixtures-base: .frontend-fixtures-base:
extends: extends:
@ -194,7 +186,7 @@ graphql-schema-dump as-if-foss:
SETUP_DB: "false" SETUP_DB: "false"
before_script: before_script:
- !reference [.default-before_script, before_script] - !reference [.default-before_script, before_script]
- !reference [.yarn-install, script] - yarn_install_script
stage: test stage: test
.jest-base: .jest-base:
@ -261,6 +253,7 @@ jest-integration:
coverage-frontend: coverage-frontend:
extends: extends:
- .default-retry - .default-retry
- .default-utils-before_script
- .yarn-cache - .yarn-cache
- .frontend:rules:coverage-frontend - .frontend:rules:coverage-frontend
needs: needs:
@ -269,9 +262,8 @@ coverage-frontend:
- job: "jest minimal" - job: "jest minimal"
optional: true optional: true
stage: post-test stage: post-test
before_script:
- !reference [.yarn-install, script]
script: script:
- yarn_install_script
- run_timed_command "yarn node scripts/frontend/merge_coverage_frontend.js" - run_timed_command "yarn node scripts/frontend/merge_coverage_frontend.js"
# Removing the individual coverage results, as we just merged them. # Removing the individual coverage results, as we just merged them.
- if ls coverage-frontend/jest-* > /dev/null 2>&1; then - if ls coverage-frontend/jest-* > /dev/null 2>&1; then
@ -291,12 +283,13 @@ coverage-frontend:
.qa-frontend-node: .qa-frontend-node:
extends: extends:
- .default-retry - .default-retry
- .default-utils-before_script
- .qa-frontend-node-cache - .qa-frontend-node-cache
- .frontend:rules:qa-frontend-node - .frontend:rules:qa-frontend-node
stage: test stage: test
needs: [] needs: []
script: script:
- !reference [.yarn-install, script] - yarn_install_script
- run_timed_command "retry yarn run webpack-prod" - run_timed_command "retry yarn run webpack-prod"
qa-frontend-node:14: qa-frontend-node:14:
@ -316,6 +309,7 @@ qa-frontend-node:latest:
webpack-dev-server: webpack-dev-server:
extends: extends:
- .default-retry - .default-retry
- .default-utils-before_script
- .yarn-cache - .yarn-cache
- .frontend:rules:default-frontend-jobs - .frontend:rules:default-frontend-jobs
stage: test stage: test
@ -324,7 +318,7 @@ webpack-dev-server:
WEBPACK_MEMORY_TEST: "true" WEBPACK_MEMORY_TEST: "true"
WEBPACK_VENDOR_DLL: "true" WEBPACK_VENDOR_DLL: "true"
script: script:
- !reference [.yarn-install, script] - yarn_install_script
- run_timed_command "retry yarn webpack-vendor" - run_timed_command "retry yarn webpack-vendor"
- run_timed_command "node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js" - run_timed_command "node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js"
artifacts: artifacts:
@ -336,13 +330,14 @@ webpack-dev-server:
bundle-size-review: bundle-size-review:
extends: extends:
- .default-retry - .default-retry
- .default-utils-before_script
- .assets-compile-cache - .assets-compile-cache
- .frontend:rules:bundle-size-review - .frontend:rules:bundle-size-review
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:danger image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:danger
stage: test stage: test
needs: [] needs: []
script: script:
- !reference [.yarn-install, script] - yarn_install_script
- scripts/bundle_size_review - scripts/bundle_size_review
artifacts: artifacts:
when: always when: always
@ -380,8 +375,8 @@ startup-css-check as-if-foss:
- .frontend-test-base - .frontend-test-base
- .storybook-yarn-cache - .storybook-yarn-cache
script: script:
- !reference [.storybook-yarn-install, script] - run_timed_command "retry yarn run storybook:install --frozen-lockfile"
- yarn run storybook:build - run_timed_command "yarn run storybook:build"
needs: ["graphql-schema-dump"] needs: ["graphql-schema-dump"]
compile-storybook: compile-storybook:

View File

@ -8,13 +8,17 @@
- job_execution_timeout - job_execution_timeout
- stuck_or_timeout_failure - stuck_or_timeout_failure
.default-before_script: .default-utils-before_script:
before_script: before_script:
- echo $FOSS_ONLY - echo $FOSS_ONLY
- '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb' - '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb'
- export GOPATH=$CI_PROJECT_DIR/.go - export GOPATH=$CI_PROJECT_DIR/.go
- mkdir -p $GOPATH - mkdir -p $GOPATH
- source scripts/utils.sh - source scripts/utils.sh
.default-before_script:
before_script:
- !reference [.default-utils-before_script, before_script]
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
.ruby-gems-cache: &ruby-gems-cache .ruby-gems-cache: &ruby-gems-cache

View File

@ -462,6 +462,12 @@ RSpec/FactoriesInMigrationSpecs:
- 'spec/lib/ee/gitlab/background_migration/**/*.rb' - 'spec/lib/ee/gitlab/background_migration/**/*.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb' - 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb'
RSpec/FactoryBot/AvoidCreate:
Enabled: true
Include:
- 'spec/serializers/**/*.rb'
- 'ee/spec/serializers/**/*.rb'
Cop/IncludeSidekiqWorker: Cop/IncludeSidekiqWorker:
Enabled: true Enabled: true
Exclude: Exclude:

View File

@ -0,0 +1,211 @@
---
RSpec/FactoryBot/AvoidCreate:
Details: grace period
Exclude:
- 'ee/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb'
- 'ee/spec/serializers/analytics/cycle_analytics/value_stream_errors_serializer_spec.rb'
- 'ee/spec/serializers/audit_event_entity_spec.rb'
- 'ee/spec/serializers/audit_event_serializer_spec.rb'
- 'ee/spec/serializers/blocking_merge_request_entity_spec.rb'
- 'ee/spec/serializers/clusters/deployment_entity_spec.rb'
- 'ee/spec/serializers/clusters/environment_entity_spec.rb'
- 'ee/spec/serializers/clusters/environment_serializer_spec.rb'
- 'ee/spec/serializers/dashboard_environment_entity_spec.rb'
- 'ee/spec/serializers/dashboard_environments_project_entity_spec.rb'
- 'ee/spec/serializers/dashboard_environments_serializer_spec.rb'
- 'ee/spec/serializers/dashboard_operations_project_entity_spec.rb'
- 'ee/spec/serializers/dependency_entity_spec.rb'
- 'ee/spec/serializers/dependency_list_serializer_spec.rb'
- 'ee/spec/serializers/ee/blob_entity_spec.rb'
- 'ee/spec/serializers/ee/build_details_entity_spec.rb'
- 'ee/spec/serializers/ee/ci/job_entity_spec.rb'
- 'ee/spec/serializers/ee/deployment_entity_spec.rb'
- 'ee/spec/serializers/ee/environment_serializer_spec.rb'
- 'ee/spec/serializers/ee/group_child_entity_spec.rb'
- 'ee/spec/serializers/ee/issue_board_entity_spec.rb'
- 'ee/spec/serializers/ee/issue_entity_spec.rb'
- 'ee/spec/serializers/ee/issue_sidebar_basic_entity_spec.rb'
- 'ee/spec/serializers/ee/issue_sidebar_extras_entity_spec.rb'
- 'ee/spec/serializers/ee/merge_request_poll_cached_widget_entity_spec.rb'
- 'ee/spec/serializers/ee/note_entity_spec.rb'
- 'ee/spec/serializers/ee/user_serializer_spec.rb'
- 'ee/spec/serializers/environment_entity_spec.rb'
- 'ee/spec/serializers/epic_entity_spec.rb'
- 'ee/spec/serializers/epic_note_entity_spec.rb'
- 'ee/spec/serializers/epics/related_epic_entity_spec.rb'
- 'ee/spec/serializers/fork_namespace_entity_spec.rb'
- 'ee/spec/serializers/geo_project_registry_entity_spec.rb'
- 'ee/spec/serializers/incident_management/escalation_policy_entity_spec.rb'
- 'ee/spec/serializers/incident_management/oncall_schedule_entity_spec.rb'
- 'ee/spec/serializers/integrations/field_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_detail_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_entity_spec.rb'
- 'ee/spec/serializers/integrations/jira_serializers/issue_serializer_spec.rb'
- 'ee/spec/serializers/integrations/zentao_serializers/issue_entity_spec.rb'
- 'ee/spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- 'ee/spec/serializers/issue_serializer_spec.rb'
- 'ee/spec/serializers/issues/linked_issue_feature_flag_entity_spec.rb'
- 'ee/spec/serializers/license_compliance/collapsed_comparer_entity_spec.rb'
- 'ee/spec/serializers/license_compliance/comparer_entity_spec.rb'
- 'ee/spec/serializers/licenses_list_entity_spec.rb'
- 'ee/spec/serializers/licenses_list_serializer_spec.rb'
- 'ee/spec/serializers/linked_feature_flag_issue_entity_spec.rb'
- 'ee/spec/serializers/member_entity_spec.rb'
- 'ee/spec/serializers/member_user_entity_spec.rb'
- 'ee/spec/serializers/merge_request_poll_widget_entity_spec.rb'
- 'ee/spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- 'ee/spec/serializers/merge_request_widget_entity_spec.rb'
- 'ee/spec/serializers/pipeline_serializer_spec.rb'
- 'ee/spec/serializers/productivity_analytics_merge_request_entity_spec.rb'
- 'ee/spec/serializers/project_mirror_entity_spec.rb'
- 'ee/spec/serializers/scim_oauth_access_token_entity_spec.rb'
- 'ee/spec/serializers/security/vulnerability_report_data_entity_spec.rb'
- 'ee/spec/serializers/status_page/incident_comment_entity_spec.rb'
- 'ee/spec/serializers/status_page/incident_entity_spec.rb'
- 'ee/spec/serializers/status_page/incident_serializer_spec.rb'
- 'ee/spec/serializers/vulnerabilities/feedback_entity_spec.rb'
- 'ee/spec/serializers/vulnerabilities/finding_entity_spec.rb'
- 'ee/spec/serializers/vulnerabilities/finding_reports_comparer_entity_spec.rb'
- 'ee/spec/serializers/vulnerabilities/finding_serializer_spec.rb'
- 'ee/spec/serializers/vulnerabilities/identifier_entity_spec.rb'
- 'ee/spec/serializers/vulnerabilities/request_entity_spec.rb'
- 'ee/spec/serializers/vulnerabilities/response_entity_spec.rb'
- 'ee/spec/serializers/vulnerabilities/scanner_entity_spec.rb'
- 'ee/spec/serializers/vulnerability_entity_spec.rb'
- 'ee/spec/serializers/vulnerability_note_entity_spec.rb'
- 'spec/serializers/access_token_entity_base_spec.rb'
- 'spec/serializers/analytics_build_entity_spec.rb'
- 'spec/serializers/analytics_build_serializer_spec.rb'
- 'spec/serializers/analytics_issue_entity_spec.rb'
- 'spec/serializers/analytics_issue_serializer_spec.rb'
- 'spec/serializers/analytics_merge_request_serializer_spec.rb'
- 'spec/serializers/analytics_summary_serializer_spec.rb'
- 'spec/serializers/base_discussion_entity_spec.rb'
- 'spec/serializers/blob_entity_spec.rb'
- 'spec/serializers/build_action_entity_spec.rb'
- 'spec/serializers/build_artifact_entity_spec.rb'
- 'spec/serializers/build_details_entity_spec.rb'
- 'spec/serializers/ci/dag_job_entity_spec.rb'
- 'spec/serializers/ci/dag_job_group_entity_spec.rb'
- 'spec/serializers/ci/dag_pipeline_entity_spec.rb'
- 'spec/serializers/ci/dag_pipeline_serializer_spec.rb'
- 'spec/serializers/ci/dag_stage_entity_spec.rb'
- 'spec/serializers/ci/downloadable_artifact_entity_spec.rb'
- 'spec/serializers/ci/downloadable_artifact_serializer_spec.rb'
- 'spec/serializers/ci/group_variable_entity_spec.rb'
- 'spec/serializers/ci/job_entity_spec.rb'
- 'spec/serializers/ci/job_serializer_spec.rb'
- 'spec/serializers/ci/lint/result_serializer_spec.rb'
- 'spec/serializers/ci/pipeline_entity_spec.rb'
- 'spec/serializers/ci/trigger_entity_spec.rb'
- 'spec/serializers/ci/variable_entity_spec.rb'
- 'spec/serializers/cluster_application_entity_spec.rb'
- 'spec/serializers/cluster_entity_spec.rb'
- 'spec/serializers/cluster_serializer_spec.rb'
- 'spec/serializers/clusters/kubernetes_error_entity_spec.rb'
- 'spec/serializers/commit_entity_spec.rb'
- 'spec/serializers/container_repositories_serializer_spec.rb'
- 'spec/serializers/container_repository_entity_spec.rb'
- 'spec/serializers/container_tag_entity_spec.rb'
- 'spec/serializers/context_commits_diff_entity_spec.rb'
- 'spec/serializers/deploy_keys/basic_deploy_key_entity_spec.rb'
- 'spec/serializers/deploy_keys/deploy_key_entity_spec.rb'
- 'spec/serializers/deployment_cluster_entity_spec.rb'
- 'spec/serializers/deployment_entity_spec.rb'
- 'spec/serializers/deployment_serializer_spec.rb'
- 'spec/serializers/diff_file_base_entity_spec.rb'
- 'spec/serializers/diff_file_entity_spec.rb'
- 'spec/serializers/diff_file_metadata_entity_spec.rb'
- 'spec/serializers/diff_viewer_entity_spec.rb'
- 'spec/serializers/diffs_entity_spec.rb'
- 'spec/serializers/diffs_metadata_entity_spec.rb'
- 'spec/serializers/discussion_diff_file_entity_spec.rb'
- 'spec/serializers/discussion_entity_spec.rb'
- 'spec/serializers/environment_entity_spec.rb'
- 'spec/serializers/environment_serializer_spec.rb'
- 'spec/serializers/environment_status_entity_spec.rb'
- 'spec/serializers/evidences/evidence_entity_spec.rb'
- 'spec/serializers/evidences/release_entity_spec.rb'
- 'spec/serializers/feature_flag_entity_spec.rb'
- 'spec/serializers/feature_flag_serializer_spec.rb'
- 'spec/serializers/feature_flag_summary_entity_spec.rb'
- 'spec/serializers/feature_flag_summary_serializer_spec.rb'
- 'spec/serializers/feature_flags_client_serializer_spec.rb'
- 'spec/serializers/fork_namespace_entity_spec.rb'
- 'spec/serializers/group_access_token_entity_spec.rb'
- 'spec/serializers/group_access_token_serializer_spec.rb'
- 'spec/serializers/group_child_entity_spec.rb'
- 'spec/serializers/group_child_serializer_spec.rb'
- 'spec/serializers/group_deploy_key_entity_spec.rb'
- 'spec/serializers/group_link/group_group_link_entity_spec.rb'
- 'spec/serializers/group_link/project_group_link_entity_spec.rb'
- 'spec/serializers/group_link/project_group_link_serializer_spec.rb'
- 'spec/serializers/impersonation_access_token_entity_spec.rb'
- 'spec/serializers/impersonation_access_token_serializer_spec.rb'
- 'spec/serializers/import/manifest_provider_repo_entity_spec.rb'
- 'spec/serializers/integrations/event_entity_spec.rb'
- 'spec/serializers/integrations/field_entity_spec.rb'
- 'spec/serializers/integrations/harbor_serializers/artifact_entity_spec.rb'
- 'spec/serializers/integrations/harbor_serializers/repository_entity_spec.rb'
- 'spec/serializers/integrations/harbor_serializers/tag_entity_spec.rb'
- 'spec/serializers/integrations/project_entity_spec.rb'
- 'spec/serializers/issuable_sidebar_extras_entity_spec.rb'
- 'spec/serializers/issue_board_entity_spec.rb'
- 'spec/serializers/issue_entity_spec.rb'
- 'spec/serializers/issue_serializer_spec.rb'
- 'spec/serializers/issue_sidebar_basic_entity_spec.rb'
- 'spec/serializers/jira_connect/app_data_serializer_spec.rb'
- 'spec/serializers/jira_connect/group_entity_spec.rb'
- 'spec/serializers/jira_connect/subscription_entity_spec.rb'
- 'spec/serializers/job_artifact_report_entity_spec.rb'
- 'spec/serializers/label_serializer_spec.rb'
- 'spec/serializers/lfs_file_lock_entity_spec.rb'
- 'spec/serializers/linked_project_issue_entity_spec.rb'
- 'spec/serializers/member_entity_spec.rb'
- 'spec/serializers/member_serializer_spec.rb'
- 'spec/serializers/member_user_entity_spec.rb'
- 'spec/serializers/merge_request_current_user_entity_spec.rb'
- 'spec/serializers/merge_request_diff_entity_spec.rb'
- 'spec/serializers/merge_request_for_pipeline_entity_spec.rb'
- 'spec/serializers/merge_request_metrics_helper_spec.rb'
- 'spec/serializers/merge_request_poll_cached_widget_entity_spec.rb'
- 'spec/serializers/merge_request_poll_widget_entity_spec.rb'
- 'spec/serializers/merge_request_serializer_spec.rb'
- 'spec/serializers/merge_request_sidebar_basic_entity_spec.rb'
- 'spec/serializers/merge_request_sidebar_extras_entity_spec.rb'
- 'spec/serializers/merge_request_user_entity_spec.rb'
- 'spec/serializers/merge_request_widget_commit_entity_spec.rb'
- 'spec/serializers/merge_request_widget_entity_spec.rb'
- 'spec/serializers/merge_requests/pipeline_entity_spec.rb'
- 'spec/serializers/namespace_basic_entity_spec.rb'
- 'spec/serializers/note_entity_spec.rb'
- 'spec/serializers/paginated_diff_entity_spec.rb'
- 'spec/serializers/personal_access_token_entity_spec.rb'
- 'spec/serializers/personal_access_token_serializer_spec.rb'
- 'spec/serializers/pipeline_details_entity_spec.rb'
- 'spec/serializers/pipeline_serializer_spec.rb'
- 'spec/serializers/project_access_token_entity_spec.rb'
- 'spec/serializers/project_access_token_serializer_spec.rb'
- 'spec/serializers/project_import_entity_spec.rb'
- 'spec/serializers/project_mirror_entity_spec.rb'
- 'spec/serializers/project_note_entity_spec.rb'
- 'spec/serializers/project_serializer_spec.rb'
- 'spec/serializers/prometheus_alert_entity_spec.rb'
- 'spec/serializers/release_serializer_spec.rb'
- 'spec/serializers/remote_mirror_entity_spec.rb'
- 'spec/serializers/review_app_setup_entity_spec.rb'
- 'spec/serializers/runner_entity_spec.rb'
- 'spec/serializers/serverless/domain_entity_spec.rb'
- 'spec/serializers/stage_entity_spec.rb'
- 'spec/serializers/stage_serializer_spec.rb'
- 'spec/serializers/suggestion_entity_spec.rb'
- 'spec/serializers/test_case_entity_spec.rb'
- 'spec/serializers/test_report_entity_spec.rb'
- 'spec/serializers/test_report_summary_entity_spec.rb'
- 'spec/serializers/test_suite_entity_spec.rb'
- 'spec/serializers/test_suite_summary_entity_spec.rb'
- 'spec/serializers/trigger_variable_entity_spec.rb'
- 'spec/serializers/user_entity_spec.rb'
- 'spec/serializers/user_serializer_spec.rb'
- 'spec/serializers/web_ide_terminal_entity_spec.rb'
- 'spec/serializers/web_ide_terminal_serializer_spec.rb'

View File

@ -1,21 +1,112 @@
<script> <script>
import { GlPagination } from '@gitlab/ui';
import { redirectTo } from '~/lib/utils/url_utility';
import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { createAlert, VARIANT_DANGER } from '~/flash';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import MessagesTable from './messages_table.vue'; import MessagesTable from './messages_table.vue';
const PER_PAGE = 20;
export default { export default {
name: 'BroadcastMessagesBase', name: 'BroadcastMessagesBase',
components: { components: {
GlPagination,
MessagesTable, MessagesTable,
}, },
props: { props: {
page: {
type: Number,
required: true,
},
messagesCount: {
type: Number,
required: true,
},
messages: { messages: {
type: Array, type: Array,
required: true, required: true,
}, },
}, },
i18n: {
deleteError: s__(
'BroadcastMessages|There was an issue deleting this message, please try again later.',
),
},
data() {
return {
currentPage: this.page,
totalMessages: this.messagesCount,
visibleMessages: this.messages.map((message) => ({
...message,
disable_delete: false,
})),
};
},
computed: {
hasVisibleMessages() {
return this.visibleMessages.length > 0;
},
},
watch: {
totalMessages(newVal, oldVal) {
// Pagination controls disappear when there is only
// one page worth of messages. Since we're relying on static data,
// this could hide messages on the next page, or leave the user
// stranded on page 2 when deleting the last message.
// Force a page reload to avoid this edge case.
if (newVal === PER_PAGE && oldVal === PER_PAGE + 1) {
redirectTo(this.buildPageUrl(1));
}
},
},
methods: {
buildPageUrl(newPage) {
return buildUrlWithCurrentLocation(`?page=${newPage}`);
},
async deleteMessage(messageId) {
const index = this.visibleMessages.findIndex((m) => m.id === messageId);
if (!index === -1) return;
const message = this.visibleMessages[index];
this.$set(this.visibleMessages, index, { ...message, disable_delete: true });
try {
await axios.delete(message.delete_path);
} catch (e) {
this.$set(this.visibleMessages, index, { ...message, disable_delete: false });
createAlert({ message: this.$options.i18n.deleteError, variant: VARIANT_DANGER });
return;
}
// Remove the message from the table
this.visibleMessages = this.visibleMessages.filter((m) => m.id !== messageId);
this.totalMessages -= 1;
},
},
}; };
</script> </script>
<template> <template>
<div> <div>
<messages-table v-if="messages.length > 0" :messages="messages" /> <messages-table
v-if="hasVisibleMessages"
:messages="visibleMessages"
@delete-message="deleteMessage"
/>
<gl-pagination
v-model="currentPage"
:total-items="totalMessages"
:link-gen="buildPageUrl"
align="center"
/>
</div> </div>
</template> </template>

View File

@ -1,10 +1,23 @@
<script> <script>
import MessagesTableRow from './messages_table_row.vue'; import { GlButton, GlTableLite, GlSafeHtmlDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!';
export default { export default {
name: 'MessagesTable', name: 'MessagesTable',
components: { components: {
MessagesTableRow, GlButton,
GlTableLite,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
i18n: {
edit: __('Edit'),
delete: __('Delete'),
}, },
props: { props: {
messages: { messages: {
@ -12,10 +25,89 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
fields() {
if (this.glFeatures.roleTargetedBroadcastMessages) return this.$options.allFields;
return this.$options.allFields.filter((f) => f.key !== 'target_roles');
},
},
allFields: [
{
key: 'status',
label: __('Status'),
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'preview',
label: __('Preview'),
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'starts_at',
label: __('Starts'),
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'ends_at',
label: __('Ends'),
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'target_roles',
label: __('Target roles'),
tdClass: DEFAULT_TD_CLASSES,
thAttr: { 'data-testid': 'target-roles-th' },
},
{
key: 'target_path',
label: __('Target Path'),
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'type',
label: __('Type'),
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'buttons',
label: '',
tdClass: `${DEFAULT_TD_CLASSES} gl-white-space-nowrap`,
},
],
safeHtmlConfig: {
ADD_TAGS: ['use'],
},
}; };
</script> </script>
<template> <template>
<div> <gl-table-lite
<messages-table-row v-for="message in messages" :key="message.id" :message="message" /> :items="messages"
</div> :fields="fields"
:tbody-tr-attr="{ 'data-testid': 'message-row' }"
stacked="md"
>
<template #cell(preview)="{ item: { preview } }">
<div v-safe-html:[$options.safeHtmlConfig]="preview"></div>
</template>
<template #cell(buttons)="{ item: { id, edit_path, disable_delete } }">
<gl-button
icon="pencil"
:aria-label="$options.i18n.edit"
:href="edit_path"
data-testid="edit-message"
/>
<gl-button
class="gl-ml-3"
icon="remove"
variant="danger"
:aria-label="$options.i18n.delete"
rel="nofollow"
:disabled="disable_delete"
:data-testid="`delete-message-${id}`"
@click="$emit('delete-message', id)"
/>
</template>
</gl-table-lite>
</template> </template>

View File

@ -1,16 +0,0 @@
<script>
export default {
name: 'MessagesTableRow',
props: {
message: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div>
{{ message.id }}
</div>
</template>

View File

@ -3,14 +3,16 @@ import BroadcastMessagesBase from './components/base.vue';
export default () => { export default () => {
const el = document.querySelector('#js-broadcast-messages'); const el = document.querySelector('#js-broadcast-messages');
const { messages } = el.dataset; const { page, messagesCount, messages } = el.dataset;
return new Vue({ return new Vue({
el, el,
name: 'BroadcastMessagesBase', name: 'BroadcastMessages',
render(createElement) { render(createElement) {
return createElement(BroadcastMessagesBase, { return createElement(BroadcastMessagesBase, {
props: { props: {
page: Number(page),
messagesCount: Number(messagesCount),
messages: JSON.parse(messages), messages: JSON.parse(messages),
}, },
}); });

View File

@ -11,6 +11,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
push_frontend_feature_flag(:vue_broadcast_messages, current_user) push_frontend_feature_flag(:vue_broadcast_messages, current_user)
push_frontend_feature_flag(:role_targeted_broadcast_messages, current_user)
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page]) @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new @broadcast_message = BroadcastMessage.new

View File

@ -22,7 +22,7 @@ module Packages
def packages_for_group_projects(installable_only: false) def packages_for_group_projects(installable_only: false)
packages = ::Packages::Package packages = ::Packages::Package
.including_project_route .including_project_namespace_route
.including_tags .including_tags
.for_projects(group_projects_visible_to_current_user.select(:id)) .for_projects(group_projects_visible_to_current_user.select(:id))
.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}") .sort_by_attribute("#{params[:order_by]}_#{params[:sort]}")

View File

@ -10,7 +10,7 @@ module Packages
@project @project
.packages .packages
.preload_pipelines .preload_pipelines
.including_project_route .including_project_namespace_route
.including_tags .including_tags
.displayable .displayable
.find(@package_id) .find(@package_id)

View File

@ -14,7 +14,7 @@ module Packages
def execute def execute
packages = project.packages packages = project.packages
.including_project_route .including_project_namespace_route
.including_tags .including_tags
packages = packages.preload_pipelines if preload_pipelines packages = packages.preload_pipelines if preload_pipelines

View File

@ -124,8 +124,8 @@ class Packages::Package < ApplicationRecord
scope :with_package_type, ->(package_type) { where(package_type: package_type) } scope :with_package_type, ->(package_type) { where(package_type: package_type) }
scope :without_package_type, ->(package_type) { where.not(package_type: package_type) } scope :without_package_type, ->(package_type) { where.not(package_type: package_type) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) } scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
scope :including_project_full_path, -> { includes(project: :route) } scope :including_project_route, -> { includes(project: :route) }
scope :including_project_route, -> { includes(project: { namespace: :route }) } scope :including_project_namespace_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) } scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) } scope :including_dependency_links, -> { includes(dependency_links: :dependency) }

View File

@ -36,6 +36,8 @@ class EventPresenter < Gitlab::View::Presenter::Delegated
'Design' 'Design'
elsif wiki_page? elsif wiki_page?
'Wiki Page' 'Wiki Page'
elsif issue? || work_item?
target.issue_type
elsif target_type.present? elsif target_type.present?
target_type.titleize target_type.titleize
else else

View File

@ -23,6 +23,7 @@ module MergeRequests
cleanup_environments(merge_request) cleanup_environments(merge_request)
abort_auto_merge(merge_request, 'merge request was closed') abort_auto_merge(merge_request, 'merge request was closed')
cleanup_refs(merge_request) cleanup_refs(merge_request)
trigger_merge_request_merge_status_updated(merge_request)
end end
merge_request merge_request
@ -38,5 +39,9 @@ module MergeRequests
merge_request_metrics_service(merge_request).close(close_event) merge_request_metrics_service(merge_request).close(close_event)
end end
end end
def trigger_merge_request_merge_status_updated(merge_request)
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
end
end end
end end

View File

@ -33,7 +33,7 @@ module Packages
min_batch_size = [batch_size, BATCH_SIZE].min min_batch_size = [batch_size, BATCH_SIZE].min
@packages.each_batch(of: min_batch_size) do |batched_packages| @packages.each_batch(of: min_batch_size) do |batched_packages|
loaded_packages = batched_packages.including_project_full_path.to_a loaded_packages = batched_packages.including_project_route.to_a
break no_access = true unless can_destroy_packages?(loaded_packages) break no_access = true unless can_destroy_packages?(loaded_packages)

View File

@ -8,7 +8,22 @@
= _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.') = _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.')
- if vue_app_enabled - if vue_app_enabled
#js-broadcast-messages{ data: { messages: @broadcast_messages.to_json } } #js-broadcast-messages{ data: {
page: params[:page] || 1,
messages_count: @broadcast_messages.total_count,
messages: @broadcast_messages.map { |message| {
id: message.id,
status: broadcast_message_status(message),
preview: broadcast_message(message, preview: true),
starts_at: message.starts_at.to_s,
ends_at: message.ends_at.to_s,
target_roles: target_access_levels_display(message.target_access_levels),
target_path: message.target_path,
type: message.broadcast_type.capitalize,
edit_path: edit_admin_broadcast_message_path(message),
delete_path: admin_broadcast_message_path(message) + '.js'
} }.to_json
} }
- else - else
= render 'form' = render 'form'
%br.clearfix %br.clearfix

View File

@ -657,7 +657,7 @@ Settings.cron_jobs['ci_runner_versions_reconciliation_worker'] ||= Settingslogic
Settings.cron_jobs['ci_runner_versions_reconciliation_worker']['cron'] ||= '@daily' Settings.cron_jobs['ci_runner_versions_reconciliation_worker']['cron'] ||= '@daily'
Settings.cron_jobs['ci_runner_versions_reconciliation_worker']['job_class'] = 'Ci::Runners::ReconcileExistingRunnerVersionsCronWorker' Settings.cron_jobs['ci_runner_versions_reconciliation_worker']['job_class'] = 'Ci::Runners::ReconcileExistingRunnerVersionsCronWorker'
Settings.cron_jobs['users_migrate_records_to_ghost_user_in_batches_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['users_migrate_records_to_ghost_user_in_batches_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['users_migrate_records_to_ghost_user_in_batches_worker']['cron'] ||= '*/1 * * * *' Settings.cron_jobs['users_migrate_records_to_ghost_user_in_batches_worker']['cron'] ||= '*/2 * * * *'
Settings.cron_jobs['users_migrate_records_to_ghost_user_in_batches_worker']['job_class'] = 'Users::MigrateRecordsToGhostUserInBatchesWorker' Settings.cron_jobs['users_migrate_records_to_ghost_user_in_batches_worker']['job_class'] = 'Users::MigrateRecordsToGhostUserInBatchesWorker'
Gitlab.ee do Gitlab.ee do

View File

@ -10,8 +10,11 @@ value_type: number
status: active status: active
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric instrumentation_class: AggregatedMetric
options: options:
aggregate:
operator: OR
attribute: user_id
events: events:
- g_edit_by_web_ide - g_edit_by_web_ide
- g_edit_by_sfe - g_edit_by_sfe

View File

@ -10,8 +10,11 @@ value_type: number
status: active status: active
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric instrumentation_class: AggregatedMetric
options: options:
aggregate:
operator: OR
attribute: user_id
events: events:
- incident_management_incident_created - incident_management_incident_created
- incident_management_incident_reopened - incident_management_incident_reopened

View File

@ -13,8 +13,11 @@ value_type: number
status: active status: active
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric instrumentation_class: AggregatedMetric
options: options:
aggregate:
operator: OR
attribute: user_id
events: events:
- i_search_total - i_search_total
- i_search_advanced - i_search_advanced

View File

@ -10,8 +10,11 @@ value_type: number
status: active status: active
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric instrumentation_class: AggregatedMetric
options: options:
aggregate:
operator: OR
attribute: user_id
events: events:
- i_quickactions_approve - i_quickactions_approve
- i_quickactions_assign_single - i_quickactions_assign_single

View File

@ -10,8 +10,11 @@ value_type: number
status: active status: active
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric instrumentation_class: AggregatedMetric
options: options:
aggregate:
operator: OR
attribute: user_id
events: events:
- i_quickactions_approve - i_quickactions_approve
- i_quickactions_assign_single - i_quickactions_assign_single

View File

@ -105,6 +105,8 @@
- 1 - 1
- - compliance_management_merge_requests_compliance_violations - - compliance_management_merge_requests_compliance_violations
- 1 - 1
- - compliance_management_update_default_framework
- 1
- - container_repository - - container_repository
- 1 - 1
- - create_commit_signature - - create_commit_signature

View File

@ -155,26 +155,38 @@ The pipeline now executes the jobs as configured.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30101) in GitLab 13.7.
You can use the [`value` and `description`](../yaml/index.md#variablesdescription) You can use the [`description` and `value`](../yaml/index.md#variablesdescription)
keywords to define keywords to define [pipeline-level (global) variables](../variables/index.md#create-a-custom-cicd-variable-in-the-gitlab-ciyml-file)
[pipeline-level (global) variables](../variables/index.md#create-a-custom-cicd-variable-in-the-gitlab-ciyml-file) that are prefilled when running a pipeline manually. Use the description to explain
that are prefilled when running a pipeline manually. what the variable is used for, what the acceptable values are, and so on.
In pipelines triggered manually, the **Run pipelines** page displays all top-level variables Job-level variables cannot be pre-filled.
with a `description` and `value` defined in the `.gitlab-ci.yml` file. The values
can then be modified if needed, which overrides the value for that single pipeline run.
The description is displayed next to the variable. It can be used to explain what In manually-triggered pipelines, the **Run pipeline** page displays all pipeline-level variables
the variable is used for, what the acceptable values are, and so on: with a `description` defined in the `.gitlab-ci.yml` file. The description displays
below the variable.
You can change the prefilled value, which overrides the value for that single pipeline run.
If you do not define a `value` for the variable in the configuration file, the variable still displays,
but the value field is blank.
For example:
```yaml ```yaml
variables: variables:
TEST_SUITE:
description: "The test suite that will run. Valid options are: 'default', 'short', 'full'."
value: "default"
DEPLOY_ENVIRONMENT: DEPLOY_ENVIRONMENT:
value: "staging" # Deploy to staging by default description: "Select the deployment target. Valid options are: 'canary', 'staging', 'production', or a stable branch of your choice."
description: "The deployment target. Change this variable to 'canary' or 'production' if needed."
``` ```
You cannot set job-level variables to be pre-filled when you run a pipeline manually. In this example:
- `TEST_SUITE` is pre-filled in the **Run pipeline** page with `default`,
and the message explains the other options.
- `DEPLOY_ENVIRONMENT` is listed in the **Run pipeline** page, but with no value set.
The user is expected to define the value each time the pipeline is run manually.
### Run a pipeline by using a URL query string ### Run a pipeline by using a URL query string

View File

@ -4200,9 +4200,9 @@ deploy_review_job:
Use the `description` keyword to define a [pipeline-level (global) variable that is prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines) Use the `description` keyword to define a [pipeline-level (global) variable that is prefilled](../pipelines/index.md#prefill-variables-in-manual-pipelines)
when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually). when [running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually).
Must be used with `value`, for the variable value. If used with `value`, the variable value is also prefilled when running a pipeline manually.
**Keyword type**: Global keyword. You cannot set job-level variables to be pre-filled when you run a pipeline manually. **Keyword type**: Global keyword. You cannot use it for job-level variables.
**Possible inputs**: **Possible inputs**:
@ -4213,10 +4213,15 @@ Must be used with `value`, for the variable value.
```yaml ```yaml
variables: variables:
DEPLOY_ENVIRONMENT: DEPLOY_ENVIRONMENT:
value: "staging"
description: "The deployment target. Change this variable to 'canary' or 'production' if needed." description: "The deployment target. Change this variable to 'canary' or 'production' if needed."
value: "staging"
``` ```
**Additional details**:
- A global variable defined with `value` but no `description` behaves the same as
[`variables`](#variables).
### `when` ### `when`
Use `when` to configure the conditions for when jobs run. If not defined in a job, Use `when` to configure the conditions for when jobs run. If not defined in a job,

View File

@ -17,7 +17,7 @@ You can restore a backup only to _the exact same version and type (CE/EE)_ of
GitLab that you created it on (for example CE 9.1.0). GitLab that you created it on (for example CE 9.1.0).
If your backup is a different version than the current installation, you must If your backup is a different version than the current installation, you must
[downgrade your GitLab installation](../update/package/downgrade.md) [downgrade](../update/package/downgrade.md) or [upgrade](../update/package/index.md#upgrade-to-a-specific-version-using-the-official-repositories) your GitLab installation
before restoring the backup. before restoring the backup.
## Restore prerequisites ## Restore prerequisites

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -6,49 +6,50 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Multiple Kubernetes clusters for Auto DevOps **(FREE)** # Multiple Kubernetes clusters for Auto DevOps **(FREE)**
When using Auto DevOps, you can deploy different environments to When using Auto DevOps, you can deploy different environments to different Kubernetes clusters.
different Kubernetes clusters, due to the 1:1 connection
[existing between them](../../user/project/clusters/multiple_kubernetes_clusters.md).
The [Deploy Job template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml) The [Deploy Job template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml) used by Auto DevOps defines three environment names:
used by Auto DevOps defines 3 environment names:
- `review/` (every environment starting with `review/`) - `review/` (every environment starting with `review/`)
- `staging` - `staging`
- `production` - `production`
Those environments are tied to jobs using [Auto Deploy](stages.md#auto-deploy), so These environments are tied to jobs using [Auto Deploy](stages.md#auto-deploy), so they must have different deployment domains. You must define separate [`KUBE_CONTEXT`](../../user/clusters/agent/ci_cd_workflow.md#using-the-agent-with-auto-devops) and [`KUBE_INGRESS_BASE_DOMAIN`](requirements.md#auto-devops-base-domain) variables for each of the three environments.
except for the environment scope, they must have a different deployment domain.
You must define a separate `KUBE_INGRESS_BASE_DOMAIN` variable for each of the above
[based on the environment](../../ci/variables/index.md#limit-the-environment-scope-of-a-cicd-variable).
The following table is an example of how to configure the three different clusters: ## Deploy to different clusters
| Cluster name | Cluster environment scope | `KUBE_INGRESS_BASE_DOMAIN` variable value | Variable environment scope | Notes | To deploy your environments to different Kubernetes clusters:
|--------------|---------------------------|-------------------------------------------|----------------------------|---|
| review | `review/*` | `review.example.com` | `review/*` | The review cluster which runs all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, used by every environment name starting with `review/`. |
| staging | `staging` | `staging.example.com` | `staging` | Optional. The staging cluster that runs the deployments of the staging environments. You must [enable it first](customize.md#deploy-policy-for-staging-and-production-environments). |
| production | `production` | `example.com` | `production` | The production cluster which runs the production environment deployments. You can use [incremental rollouts](customize.md#incremental-rollout-to-production). |
To add a different cluster for each environment: 1. [Create Kubernetes clusters](../../user/infrastructure/clusters/connect/new_gke_cluster.md).
1. Associate the clusters to your project:
1. [Install a GitLab Agent on each cluster](../../user/clusters/agent/index.md).
1. [Configure each agent to access your project](../../user/clusters/agent/install/index.md#configure-your-agent).
1. [Install NGINX Ingress Controller](cloud_deployments/auto_devops_with_gke.md#install-ingress) in each cluster. Save the IP address and Kubernetes namespace for the next step.
1. [Configure the Auto DevOps CI/CD Pipeline variables](customize.md#build-and-deployment)
- Set up a `KUBE_CONTEXT` variable [for each environment](../../ci/variables/index.md#limit-the-environment-scope-of-a-cicd-variable). The value must point to the agent of the relevant cluster.
- Set up a `KUBE_INGRESS_BASE_DOMAIN`. You must [configure the base domain](requirements.md#auto-devops-base-domain) for each environment to point to the Ingress of the relevant cluster.
- Add a `KUBE_NAMESPACE` variable with a value of the Kubernetes namespace you want your deployments to target. You can scope the variable to multiple environments.
1. Navigate to your project's **Infrastructure > Kubernetes clusters**. For deprecated, [certificate-based clusters](../../user/infrastructure/clusters/index.md#certificate-based-kubernetes-integration-deprecated):
1. Create the Kubernetes clusters with their respective environment scope, as
described from the table above. 1. Go to the project and select **Infrastructure > Kubernetes clusters** from the left sidebar.
1. After creating the clusters, navigate to each cluster and 1. [Set the environment scope of each cluster](../../user/project/clusters/multiple_kubernetes_clusters.md#setting-the-environment-scope).
[install Ingress](cloud_deployments/auto_devops_with_gke.md#install-ingress). 1. For each cluster, [add a domain based on its Ingress IP address](../../user/project/clusters/gitlab_managed_clusters.md#base-domain).
Wait for the Ingress IP address to be assigned.
1. Make sure you've [configured your DNS](requirements.md#auto-devops-base-domain) with the NOTE:
specified Auto DevOps domains. [Cluster environment scope is not respected when checking for active Kubernetes clusters](https://gitlab.com/gitlab-org/gitlab/-/issues/20351). For a multi-cluster setup to work with Auto DevOps, you must create a fallback cluster with **Cluster environment scope** set to `*`. You can set any of the clusters you've already added as a fallback cluster.
1. Navigate to each cluster's page, through **Infrastructure > Kubernetes clusters**,
and add the domain based on its Ingress IP address. ### Example configurations
| Cluster name | Cluster environment scope | `KUBE_INGRESS_BASE_DOMAIN` value | `KUBE CONTEXT` value | Variable environment scope | Notes |
| :------------| :-------------------------| :------------------------------- | :--------------------------------- | :--------------------------|:--|
| review | `review/*` | `review.example.com` | `path/to/project:review-agent` | `review/*` | A review cluster that runs all [Review Apps](../../ci/review_apps/index.md).|
| staging | `staging` | `staging.example.com` | `path/to/project:staging-agent` | `staging` | Optional. A staging cluster that runs the deployments of the staging environments. You must [enable it first](customize.md#deploy-policy-for-staging-and-production-environments). |
| production | `production` | `example.com` | `path/to/project:production-agent` | `production` | A production cluster that runs the production environment deployments. You can use [incremental rollouts](customize.md#incremental-rollout-to-production). |
## Test your configuration
After completing configuration, test your setup by creating a merge request. After completing configuration, test your setup by creating a merge request.
Verify whether your application deployed as a Review App in the Kubernetes Verify whether your application deployed as a Review App in the Kubernetes
cluster with the `review/*` environment scope. Similarly, check the cluster with the `review/*` environment scope. Similarly, check the
other environments. other environments.
[Cluster environment scope isn't respected](https://gitlab.com/gitlab-org/gitlab/-/issues/20351)
when checking for active Kubernetes clusters. For multi-cluster setup to work with Auto DevOps,
create a fallback cluster with **Cluster environment scope** set to `*`. A new cluster isn't
required. You can use any of the clusters already added.

View File

@ -146,7 +146,7 @@ the timeline header represent the days of the week.
The timeline bar indicates the approximate position of an epic or milestone based on its start and The timeline bar indicates the approximate position of an epic or milestone based on its start and
due dates. due dates.
## Blocked epics ## Blocked epics **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33587) in GitLab 15.5: View blocking epics when hovering over the “blocked” icon. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33587) in GitLab 15.5: View blocking epics when hovering over the “blocked” icon.

View File

@ -161,9 +161,7 @@ module API
end end
end end
desc 'Delete deploy key for a project' do desc 'Delete deploy key for a project'
success Key
end
params do params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key' requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end end

View File

@ -54,7 +54,7 @@ module API
params do params do
requires :path, type: String, desc: 'Group path' requires :path, type: String, desc: 'Group path'
requires :name, type: String, desc: 'Group name' requires :name, type: String, desc: 'Group name'
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The group export file to be imported' requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The group export file to be imported', documentation: { type: 'file' }
optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace." optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace."
end end
post 'import' do post 'import' do

View File

@ -91,7 +91,7 @@ module API
end end
params do params do
requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex
requires :chart, type: ::API::Validations::Types::WorkhorseFile, desc: 'The chart file to be published (generated by Multipart middleware)' requires :chart, type: ::API::Validations::Types::WorkhorseFile, desc: 'The chart file to be published (generated by Multipart middleware)', documentation: { type: 'file' }
end end
post "api/:channel/charts" do post "api/:channel/charts" do
authorize_upload!(authorized_user_project) authorize_upload!(authorized_user_project)

View File

@ -11,7 +11,7 @@ module API
optional :visibility, type: String, optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values, values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the group' desc: 'The visibility of the group'
optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for the group' optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for the group', documentation: { type: 'file' }
optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group' optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group'
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users in this group to setup Two-factor authentication' optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users in this group to setup Two-factor authentication'
optional :two_factor_grace_period, type: Integer, desc: 'Time before Two-factor authentication is enforced' optional :two_factor_grace_period, type: Integer, desc: 'Time before Two-factor authentication is enforced'

View File

@ -3,7 +3,7 @@
module Gitlab module Gitlab
module Utils module Utils
class ExecutionTracker class ExecutionTracker
MAX_RUNTIME = 30.seconds MAX_RUNTIME = 60.seconds
ExecutionTimeOutError = Class.new(StandardError) ExecutionTimeOutError = Class.new(StandardError)

View File

@ -7056,6 +7056,9 @@ msgstr ""
msgid "Broadcast Messages" msgid "Broadcast Messages"
msgstr "" msgstr ""
msgid "BroadcastMessages|There was an issue deleting this message, please try again later."
msgstr ""
msgid "Browse Directory" msgid "Browse Directory"
msgstr "" msgstr ""
@ -12254,6 +12257,9 @@ msgstr ""
msgid "DastProfiles|Save profile" msgid "DastProfiles|Save profile"
msgstr "" msgstr ""
msgid "DastProfiles|Scan Method"
msgstr ""
msgid "DastProfiles|Scan method" msgid "DastProfiles|Scan method"
msgstr "" msgstr ""

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'rubocop-rspec'
module RuboCop
module Cop
module RSpec
module FactoryBot
# This cop checks for the creation of ActiveRecord objects in serializers specs specs
#
# @example
#
# # bad
# let(:user) { create(:user) }
# let(:users) { create_list(:user, 2) }
#
# # good
# let(:user) { build_stubbed(:user) }
# let(:user) { build(:user) }
# let(:users) { build_stubbed_list(:user, 2) }
# let(:users) { build_list(:user, 2) }
class AvoidCreate < RuboCop::Cop::Base
MESSAGE = "Prefer using `build_stubbed` or similar over `%{method_name}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#optimize-factory-usage"
FORBIDDEN_METHODS = %i[create create_list].freeze
RESTRICT_ON_SEND = FORBIDDEN_METHODS
def_node_matcher :forbidden_factory_usage, <<~PATTERN
(
send
{(const nil? :FactoryBot) nil?}
${ #{FORBIDDEN_METHODS.map(&:inspect).join(' ')} }
...
)
PATTERN
def on_send(node)
method_name = forbidden_factory_usage(node)
return unless method_name
add_offense(node, message: format(MESSAGE, method_name: method_name))
end
end
end
end
end
end

View File

@ -30,12 +30,42 @@ require_relative '../config/bundler_setup'
require 'rubocop' require 'rubocop'
require 'optparse' require 'optparse'
def print_ast(file, source, version) module Helper
version ||= RuboCop::ConfigStore.new.for_file(file).target_ruby_version extend self
class << self
attr_writer :ruby_version
end
def ast(source, file: '', version: nil)
version ||= ruby_version
puts RuboCop::AST::ProcessedSource.new(source, version).ast.to_s puts RuboCop::AST::ProcessedSource.new(source, version).ast.to_s
end end
options = Struct.new(:eval, :ruby_version, :print_help, keyword_init: true).new def ruby_version
@ruby_version ||= rubocop_target_ruby_version
end
def rubocop_target_ruby_version
@rubocop_target_ruby_version ||= RuboCop::ConfigStore.new.for_file('.').target_ruby_version
end
end
def start_irb
require 'irb'
include Helper # rubocop:disable Style/MixinUsage
puts "Ruby version: #{ruby_version}"
puts
puts "Use `ast(source_string, version: nil)` method to parse code and output AST. For example:"
puts " ast('puts :hello')"
puts
IRB.start
end
options = Struct.new(:eval, :interactive, :print_help, keyword_init: true).new
parser = OptionParser.new do |opts| parser = OptionParser.new do |opts|
opts.banner = "Usage: #{$PROGRAM_NAME} [-e code] [FILE...]" opts.banner = "Usage: #{$PROGRAM_NAME} [-e code] [FILE...]"
@ -44,9 +74,13 @@ parser = OptionParser.new do |opts|
options.eval = code options.eval = code
end end
opts.on('-i', '--interactive', '') do
options.interactive = true
end
opts.on('-v RUBY_VERSION', '--ruby-version RUBY_VERSION', opts.on('-v RUBY_VERSION', '--ruby-version RUBY_VERSION',
'Parse as Ruby would. Defaults to RuboCop TargetRubyVersion setting.') do |ruby_version| 'Parse as Ruby would. Defaults to RuboCop TargetRubyVersion setting.') do |ruby_version|
options.ruby_version = Float(ruby_version) Helper.ruby_version = Float(ruby_version)
end end
opts.on('-h', '--help') do opts.on('-h', '--help') do
@ -54,20 +88,31 @@ parser = OptionParser.new do |opts|
end end
end end
args = parser.parse! files = parser.parse!
if options.print_help if options.print_help
puts parser puts parser
exit elsif options.interactive
end if options.eval || files.any?
puts "Cannot combine `--interactive` with `--eval` or passing files. Aborting..."
puts
print_ast('', options.eval, options.ruby_version) if options.eval puts parser
exit 1
args.each do |arg|
if File.file?(arg)
source = File.read(arg)
print_ast(arg, source, options.ruby_version)
else else
warn "Skipping non-file #{arg.inspect}" start_irb
end
elsif options.eval
Helper.ast(options.eval)
elsif files.any?
files.each do |file|
if File.file?(file)
source = File.read(file)
Helper.ast(source, file: file)
else
warn "Skipping non-file #{file.inspect}"
end end
end end
else
puts parser
end

View File

@ -54,6 +54,16 @@ FactoryBot.define do
target { note } target { note }
end end
trait :for_issue do
target { association(:issue, issue_type: :issue) }
target_type { 'Issue' }
end
trait :for_work_item do
target { association(:work_item, :task) }
target_type { 'WorkItem' }
end
factory :design_event, traits: [:has_design] do factory :design_event, traits: [:has_design] do
action { :created } action { :created }
target { design } target { design }

View File

@ -1,35 +1,112 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue'; import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue'; import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
import { MOCK_MESSAGES } from '../mock_data'; import { generateMockMessages, MOCK_MESSAGES } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('BroadcastMessagesBase', () => { describe('BroadcastMessagesBase', () => {
let wrapper; let wrapper;
let axiosMock;
useMockLocationHelper();
const findTable = () => wrapper.findComponent(MessagesTable); const findTable = () => wrapper.findComponent(MessagesTable);
const findPagination = () => wrapper.findComponent(GlPagination);
function createComponent(props = {}) { function createComponent(props = {}) {
wrapper = shallowMount(BroadcastMessagesBase, { wrapper = shallowMount(BroadcastMessagesBase, {
propsData: { propsData: {
page: 1,
messagesCount: MOCK_MESSAGES.length,
messages: MOCK_MESSAGES, messages: MOCK_MESSAGES,
...props, ...props,
}, },
}); });
} }
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => { afterEach(() => {
axiosMock.restore();
wrapper.destroy(); wrapper.destroy();
}); });
it('renders the table when there are existing messages', () => { it('renders the table and pagination when there are existing messages', () => {
createComponent(); createComponent();
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findPagination().exists()).toBe(true);
}); });
it('does not render the table when there are no existing messages', () => { it('does not render the table when there are no visible messages', () => {
createComponent({ messages: [] }); createComponent({ messages: [] });
expect(findTable().exists()).toBe(false); expect(findTable().exists()).toBe(false);
expect(findPagination().exists()).toBe(true);
});
it('does not remove a deleted message if it was not in visibleMessages', async () => {
createComponent();
findTable().vm.$emit('delete-message', -1);
await waitForPromises();
expect(axiosMock.history.delete).toHaveLength(0);
expect(wrapper.vm.visibleMessages.length).toBe(MOCK_MESSAGES.length);
});
it('does not remove a deleted message if the request fails', async () => {
createComponent();
const { id, delete_path } = MOCK_MESSAGES[0];
axiosMock.onDelete(delete_path).replyOnce(500);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
expect(wrapper.vm.visibleMessages.find((m) => m.id === id)).not.toBeUndefined();
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: BroadcastMessagesBase.i18n.deleteError,
}),
);
});
it('removes a deleted message from visibleMessages on success', async () => {
createComponent();
const { id, delete_path } = MOCK_MESSAGES[0];
axiosMock.onDelete(delete_path).replyOnce(200);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
expect(wrapper.vm.visibleMessages.find((m) => m.id === id)).toBeUndefined();
expect(wrapper.vm.totalMessages).toBe(MOCK_MESSAGES.length - 1);
});
it('redirects to the first page when totalMessages changes from 21 to 20', async () => {
window.location.pathname = `${TEST_HOST}/admin/broadcast_messages`;
const messages = generateMockMessages(21);
const { id, delete_path } = messages[0];
createComponent({ messages, messagesCount: messages.length });
axiosMock.onDelete(delete_path).replyOnce(200);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
expect(redirectTo).toHaveBeenCalledWith(`${TEST_HOST}/admin/broadcast_messages?page=1`);
}); });
}); });

View File

@ -1,26 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import MessagesTableRow from '~/admin/broadcast_messages/components/messages_table_row.vue';
import { MOCK_MESSAGE } from '../mock_data';
describe('MessagesTableRow', () => {
let wrapper;
function createComponent(props = {}) {
wrapper = shallowMount(MessagesTableRow, {
propsData: {
message: MOCK_MESSAGE,
...props,
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders the message ID', () => {
createComponent();
expect(wrapper.text()).toBe(`${MOCK_MESSAGE.id}`);
});
});

View File

@ -1,15 +1,19 @@
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue'; import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
import MessagesTableRow from '~/admin/broadcast_messages/components/messages_table_row.vue';
import { MOCK_MESSAGES } from '../mock_data'; import { MOCK_MESSAGES } from '../mock_data';
describe('MessagesTable', () => { describe('MessagesTable', () => {
let wrapper; let wrapper;
const findRows = () => wrapper.findAllComponents(MessagesTableRow); const findRows = () => wrapper.findAll('[data-testid="message-row"]');
const findTargetRoles = () => wrapper.find('[data-testid="target-roles-th"]');
const findDeleteButton = (id) => wrapper.find(`[data-testid="delete-message-${id}"]`);
function createComponent(props = {}) { function createComponent(props = {}, glFeatures = {}) {
wrapper = shallowMount(MessagesTable, { wrapper = mount(MessagesTable, {
provide: {
glFeatures,
},
propsData: { propsData: {
messages: MOCK_MESSAGES, messages: MOCK_MESSAGES,
...props, ...props,
@ -26,4 +30,22 @@ describe('MessagesTable', () => {
expect(findRows()).toHaveLength(MOCK_MESSAGES.length); expect(findRows()).toHaveLength(MOCK_MESSAGES.length);
}); });
it('renders the "Target Roles" column when roleTargetedBroadcastMessages is enabled', () => {
createComponent({}, { roleTargetedBroadcastMessages: true });
expect(findTargetRoles().exists()).toBe(true);
});
it('does not render the "Target Roles" column when roleTargetedBroadcastMessages is disabled', () => {
createComponent();
expect(findTargetRoles().exists()).toBe(false);
});
it('emits a delete-message event when a delete button is clicked', () => {
const { id } = MOCK_MESSAGES[0];
createComponent();
findDeleteButton(id).element.click();
expect(wrapper.emitted('delete-message')).toHaveLength(1);
expect(wrapper.emitted('delete-message')[0]).toEqual([id]);
});
}); });

View File

@ -1,5 +1,17 @@
export const MOCK_MESSAGE = { const generateMockMessage = (id) => ({
id: 100, id,
}; delete_path: `/admin/broadcast_messages/${id}.js`,
edit_path: `/admin/broadcast_messages/${id}/edit`,
starts_at: new Date().toISOString(),
ends_at: new Date().toISOString(),
preview: '<div>YEET</div>',
status: 'Expired',
target_path: '*/welcome',
target_roles: 'Maintainer, Owner',
type: 'Banner',
});
export const MOCK_MESSAGES = [MOCK_MESSAGE, { id: 200 }, { id: 300 }]; export const generateMockMessages = (n) =>
[...Array(n).keys()].map((id) => generateMockMessage(id + 1));
export const MOCK_MESSAGES = generateMockMessages(5).map((id) => generateMockMessage(id));

View File

@ -314,15 +314,15 @@ RSpec.describe GitlabSchema do
end end
describe '.parse_gids' do describe '.parse_gids' do
let_it_be(:global_ids) { %w[gid://gitlab/TestOne/123 gid://gitlab/TestOne/456] } let_it_be(:global_ids) { %w[gid://gitlab/TestOne/123 gid://gitlab/TestTwo/456] }
subject(:parse_gids) { described_class.parse_gids(global_ids, expected_type: TestOne) } subject(:parse_gids) { described_class.parse_gids(global_ids, expected_type: [TestOne, TestTwo]) }
it 'parses the gids' do it 'parses the gids' do
expect(described_class).to receive(:parse_gid).with('gid://gitlab/TestOne/123', expected_type: TestOne).and_call_original expect(described_class).to receive(:parse_gid).with('gid://gitlab/TestOne/123', expected_type: [TestOne, TestTwo]).and_call_original
expect(described_class).to receive(:parse_gid).with('gid://gitlab/TestOne/456', expected_type: TestOne).and_call_original expect(described_class).to receive(:parse_gid).with('gid://gitlab/TestTwo/456', expected_type: [TestOne, TestTwo]).and_call_original
expect(parse_gids.map(&:model_id)).to eq %w[123 456] expect(parse_gids.map(&:model_id)).to eq %w[123 456]
expect(parse_gids.map(&:model_class)).to match_array [TestOne, TestOne] expect(parse_gids.map(&:model_class)).to eq [TestOne, TestTwo]
end end
end end
end end

View File

@ -51,6 +51,14 @@ RSpec.describe EventPresenter do
it 'returns milestone for a milestone event' do it 'returns milestone for a milestone event' do
expect(group_event.present).to have_attributes(target_type_name: 'milestone') expect(group_event.present).to have_attributes(target_type_name: 'milestone')
end end
it 'returns the issue_type for issue events' do
expect(build(:event, :for_issue, :created).present).to have_attributes(target_type_name: 'issue')
end
it 'returns the issue_type for work item events' do
expect(build(:event, :for_work_item, :created).present).to have_attributes(target_type_name: 'task')
end
end end
describe '#note_target_type_name' do describe '#note_target_type_name' do

View File

@ -438,23 +438,9 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'processing HEAD requests', instance_level: true it_behaves_like 'processing HEAD requests', instance_level: true
end end
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'handling groups, subgroups and user namespaces for', 'heading a file' it_behaves_like 'handling groups, subgroups and user namespaces for', 'heading a file'
end end
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'handling groups, subgroups and user namespaces for', 'heading a file'
end
end
describe 'GET /api/v4/groups/:id/-/packages/maven/*path/:file_name' do describe 'GET /api/v4/groups/:id/-/packages/maven/*path/:file_name' do
before do before do
project.team.truncate project.team.truncate
@ -668,23 +654,9 @@ RSpec.describe API::MavenPackages do
let(:path) { package.maven_metadatum.path } let(:path) { package.maven_metadatum.path }
let(:url) { "/groups/#{group.id}/-/packages/maven/#{path}/#{package_file.file_name}" } let(:url) { "/groups/#{group.id}/-/packages/maven/#{path}/#{package_file.file_name}" }
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'handling groups and subgroups for', 'processing HEAD requests' it_behaves_like 'handling groups and subgroups for', 'processing HEAD requests'
end end
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'handling groups and subgroups for', 'processing HEAD requests'
end
end
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
context 'a public project' do context 'a public project' do
subject { download_file(file_name: package_file.file_name) } subject { download_file(file_name: package_file.file_name) }
@ -774,23 +746,9 @@ RSpec.describe API::MavenPackages do
let(:path) { package.maven_metadatum.path } let(:path) { package.maven_metadatum.path }
let(:url) { "/projects/#{project.id}/packages/maven/#{path}/#{package_file.file_name}" } let(:url) { "/projects/#{project.id}/packages/maven/#{path}/#{package_file.file_name}" }
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
it_behaves_like 'processing HEAD requests' it_behaves_like 'processing HEAD requests'
end end
context 'with check_maven_path_first disabled' do
before do
stub_feature_flags(check_maven_path_first: false)
end
it_behaves_like 'processing HEAD requests'
end
end
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do
it 'rejects a malicious request' do it 'rejects a malicious request' do
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), headers: headers_with_token put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), headers: headers_with_token

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
require_relative '../../../../../rubocop/cop/rspec/factory_bot/avoid_create'
RSpec.describe RuboCop::Cop::RSpec::FactoryBot::AvoidCreate do
shared_examples 'an offensive factory call' do |namespace|
%i[create create_list].each do |forbidden_method|
namespaced_forbidden_method = "#{namespace}#{forbidden_method}(:user)"
it "registers an offense for #{namespaced_forbidden_method}" do
expect_offense(<<-RUBY)
describe 'foo' do
let(:user) { #{namespaced_forbidden_method} }
#{'^' * namespaced_forbidden_method.size} Prefer using `build_stubbed` or similar over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#optimize-factory-usage
end
RUBY
end
end
end
it_behaves_like 'an offensive factory call', ''
it_behaves_like 'an offensive factory call', 'FactoryBot.'
end

View File

@ -9,6 +9,7 @@ RSpec.describe MergeRequests::CloseService do
let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) } let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project } let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
let(:service) { described_class.new(project: project, current_user: user) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
@ -16,18 +17,20 @@ RSpec.describe MergeRequests::CloseService do
project.add_guest(guest) project.add_guest(guest)
end end
def execute
service.execute(merge_request)
end
describe '#execute' do describe '#execute' do
it_behaves_like 'cache counters invalidator' it_behaves_like 'cache counters invalidator'
it_behaves_like 'merge request reviewers cache counters invalidator' it_behaves_like 'merge request reviewers cache counters invalidator'
context 'valid params' do context 'valid params' do
let(:service) { described_class.new(project: project, current_user: user) }
before do before do
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
perform_enqueued_jobs do perform_enqueued_jobs do
@merge_request = service.execute(merge_request) @merge_request = execute
end end
end end
@ -73,7 +76,7 @@ RSpec.describe MergeRequests::CloseService do
expect(metrics_service).to receive(:close) expect(metrics_service).to receive(:close)
described_class.new(project: project, current_user: user).execute(merge_request) execute
end end
it 'calls the merge request activity counter' do it 'calls the merge request activity counter' do
@ -81,13 +84,11 @@ RSpec.describe MergeRequests::CloseService do
.to receive(:track_close_mr_action) .to receive(:track_close_mr_action)
.with(user: user) .with(user: user)
described_class.new(project: project, current_user: user).execute(merge_request) execute
end end
it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
service = described_class.new(project: project, current_user: user) expect { execute }
expect { service.execute(merge_request) }
.to change { project.open_merge_requests_count }.from(1).to(0) .to change { project.open_merge_requests_count }.from(1).to(0)
end end
@ -96,25 +97,39 @@ RSpec.describe MergeRequests::CloseService do
expect(service).to receive(:execute_for_merge_request_pipeline).with(merge_request) expect(service).to receive(:execute_for_merge_request_pipeline).with(merge_request)
end end
described_class.new(project: project, current_user: user).execute(merge_request) execute
end end
it 'schedules CleanupRefsService' do it 'schedules CleanupRefsService' do
expect(MergeRequests::CleanupRefsService).to receive(:schedule).with(merge_request) expect(MergeRequests::CleanupRefsService).to receive(:schedule).with(merge_request)
described_class.new(project: project, current_user: user).execute(merge_request) execute
end
it 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(merge_request)
execute
end end
context 'current user is not authorized to close merge request' do context 'current user is not authorized to close merge request' do
let(:user) { guest }
before do before do
perform_enqueued_jobs do perform_enqueued_jobs do
@merge_request = described_class.new(project: project, current_user: guest).execute(merge_request) @merge_request = execute
end end
end end
it 'does not close the merge request' do it 'does not close the merge request' do
expect(@merge_request).to be_open expect(@merge_request).to be_open
end end
it 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated)
execute
end
end end
end end
end end

View File

@ -425,11 +425,6 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
create(:merge_request, :simple, source_project: project, reviewer_ids: [user2.id]) create(:merge_request, :simple, source_project: project, reviewer_ids: [user2.id])
end end
context 'when merge_request_reviewer feature is enabled' do
before do
stub_feature_flags(merge_request_reviewer: true)
end
let(:opts) { { reviewer_ids: [IssuableFinder::Params::NONE] } } let(:opts) { { reviewer_ids: [IssuableFinder::Params::NONE] } }
it 'removes reviewers' do it 'removes reviewers' do
@ -437,7 +432,6 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end end
end end
end end
end
context 'after_save callback to store_mentions' do context 'after_save callback to store_mentions' do
let(:merge_request) { create(:merge_request, title: 'Old title', description: "simple description", source_branch: 'test', source_project: project, author: user) } let(:merge_request) { create(:merge_request, title: 'Old title', description: "simple description", source_branch: 'test', source_project: project, author: user) }

View File

@ -19,11 +19,6 @@ RSpec.shared_examples 'reviewer_ids filter' do
let(:reviewer2) { create(:user) } let(:reviewer2) { create(:user) }
context 'when the current user can admin the merge_request' do context 'when the current user can admin the merge_request' do
context 'when merge_request_reviewer feature is enabled' do
before do
stub_feature_flags(merge_request_reviewer: true)
end
context 'with a reviewer who can read the merge_request' do context 'with a reviewer who can read the merge_request' do
before do before do
project.add_developer(reviewer1) project.add_developer(reviewer1)
@ -35,17 +30,6 @@ RSpec.shared_examples 'reviewer_ids filter' do
end end
end end
context 'when merge_request_reviewer feature is disabled' do
before do
stub_feature_flags(merge_request_reviewer: false)
end
it 'contains no reviewers' do
expect(execute.reviewers).to eq []
end
end
end
context 'when the current_user cannot admin the merge_request' do context 'when the current_user cannot admin the merge_request' do
before do before do
project.add_developer(user) project.add_developer(user)

View File

@ -7,33 +7,41 @@ RSpec.describe 'events/event/_common.html.haml' do
let_it_be(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
context 'when it is a work item event' do before do
let(:work_item) { create(:work_item, project: project) } render partial: 'events/event/common', locals: { event: event.present }
end
let(:event) do context 'when it is a work item event' do
let_it_be(:work_item) { create(:work_item, :task, project: project) }
let_it_be(:event) do
create(:event, :created, project: project, target: work_item, target_type: 'WorkItem', author: user) create(:event, :created, project: project, target: work_item, target_type: 'WorkItem', author: user)
end end
it 'renders the correct url' do it 'renders the correct url' do
render partial: 'events/event/common', locals: { event: event.present }
expect(rendered).to have_link( expect(rendered).to have_link(
work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.id}" work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.id}"
) )
end end
it 'uses issue_type for the target_name' do
expect(rendered).to have_content("#{s_('Event|opened')} task #{work_item.to_reference}")
end
end end
context 'when it is an isssue event' do context 'when it is an issue event' do
let(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let(:event) do let_it_be(:event) do
create(:event, :created, project: project, target: issue, author: user) create(:event, :created, project: project, target: issue, author: user)
end end
it 'renders the correct url' do it 'renders the correct url' do
render partial: 'events/event/common', locals: { event: event.present }
expect(rendered).to have_link(issue.reference_link_text, href: "/#{project.full_path}/-/issues/#{issue.iid}") expect(rendered).to have_link(issue.reference_link_text, href: "/#{project.full_path}/-/issues/#{issue.iid}")
end end
it 'uses issue_type for the target_name' do
expect(rendered).to have_content("#{s_('Event|opened')} issue #{issue.to_reference}")
end
end end
end end