summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-13 18:09:27 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-13 18:09:27 +0000
commit5248c5e2212b8e42b28b23e6839d69db0006829b (patch)
treef989d4b4cd06fc5dc28c024a5f230b42b0af179b
parent0d55697d64b5f053bbd0f69da2962e7478097de3 (diff)
downloadgitlab-ce-5248c5e2212b8e42b28b23e6839d69db0006829b.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rwxr-xr-x.lefthook/pre-push/merge_conflicts23
-rw-r--r--.rubocop_todo/gitlab/strong_memoize_attr.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/base.vue5
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue47
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue225
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue34
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue14
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/constants.js35
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/edit.js43
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/index.js5
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue7
-rw-r--r--app/assets/javascripts/badges/constants.js8
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue14
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list.vue3
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue55
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index/index.js (renamed from app/assets/javascripts/pages/admin/broadcast_messages/index.js)2
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js2
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/repository/index.js31
-rw-r--r--app/assets/javascripts/repository/utils/ref_switcher_utils.js30
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue7
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss7
-rw-r--r--app/controllers/concerns/verifies_with_email.rb8
-rw-r--r--app/controllers/search_controller.rb5
-rw-r--r--app/graphql/mutations/ci/pipeline_schedule/play.rb32
-rw-r--r--app/graphql/resolvers/ci/runner_groups_resolver.rb51
-rw-r--r--app/graphql/resolvers/ci/runner_jobs_resolver.rb9
-rw-r--r--app/graphql/resolvers/ci/runner_owner_project_resolver.rb26
-rw-r--r--app/graphql/types/ci/runner_type.rb39
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/ci/build.rb11
-rw-r--r--app/models/ci/job_token/allowlist.rb42
-rw-r--r--app/models/ci/job_token/project_scope_link.rb4
-rw-r--r--app/models/ci/job_token/scope.rb59
-rw-r--r--app/models/ci/runner_namespace.rb2
-rw-r--r--app/models/group.rb1
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/search_service_presenter.rb6
-rw-r--r--app/services/ci/pipeline_schedule_service.rb2
-rw-r--r--app/services/projects/create_service.rb3
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/import_service.rb3
-rw-r--r--app/services/search_service.rb6
-rw-r--r--app/views/admin/broadcast_messages/edit.html.haml17
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml1
-rw-r--r--app/views/groups/milestones/_form.html.haml6
-rw-r--r--app/views/import/manifest/_form.html.haml7
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml4
-rw-r--r--app/views/projects/tree/_tree_header.html.haml2
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/show.html.haml2
-rw-r--r--config/feature_categories.yml2
-rw-r--r--config/feature_flags/development/check_ip_address_for_email_verification.yml8
-rw-r--r--config/feature_flags/development/ci_build_partition_id_token_prefix.yml8
-rw-r--r--config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml8
-rw-r--r--config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml8
-rw-r--r--config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml8
-rw-r--r--doc/api/graphql/reference/index.md21
-rw-r--r--doc/api/group_badges.md2
-rw-r--r--doc/api/project_badges.md2
-rw-r--r--doc/ci/variables/index.md2
-rw-r--r--doc/development/integrations/secure.md2
-rw-r--r--doc/integration/saml.md16
-rw-r--r--doc/topics/autodevops/cicd_variables.md331
-rw-r--r--doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md6
-rw-r--r--doc/topics/autodevops/customize.md348
-rw-r--r--doc/topics/autodevops/multiple_clusters_auto_devops.md6
-rw-r--r--doc/topics/autodevops/prepare_deployment.md4
-rw-r--r--doc/topics/autodevops/requirements.md4
-rw-r--r--doc/topics/autodevops/stages.md4
-rw-r--r--doc/topics/autodevops/troubleshooting.md2
-rw-r--r--doc/topics/autodevops/upgrading_auto_deploy_dependencies.md4
-rw-r--r--doc/user/markdown.md4
-rw-r--r--doc/user/packages/pypi_repository/index.md26
-rw-r--r--doc/user/project/badges.md2
-rw-r--r--doc/user/project/canary_deployments.md2
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md2
-rw-r--r--doc/user/project/merge_requests/reviews/img/suggestion_code_block_editor_v12_8.pngbin9917 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/reviews/suggestions.md8
-rw-r--r--lefthook.yml3
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb34
-rw-r--r--lib/gitlab/ci/pipeline/seed/pipeline.rb5
-rw-r--r--lib/gitlab/github_gists_import/importer/gists_importer.rb95
-rw-r--r--lib/gitlab/github_import/page_counter.rb6
-rw-r--r--lib/gitlab/graphql/limit/field_call_count.rb13
-rw-r--r--lib/gitlab/project_template.rb3
-rw-r--r--locale/gitlab.pot114
-rw-r--r--package.json2
-rw-r--r--qa/lib/gitlab/page/group/settings/usage_quotas.rb8
-rw-r--r--spec/features/admin/admin_runners_spec.rb16
-rw-r--r--spec/features/alert_management/alert_details_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb4
-rw-r--r--spec/features/jira_connect/branches_spec.rb20
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb28
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb15
-rw-r--r--spec/features/users/email_verification_on_login_spec.rb8
-rw-r--r--spec/finders/ci/auth_job_finder_spec.rb4
-rw-r--r--spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js46
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js201
-rw-r--r--spec/frontend/admin/broadcast_messages/mock_data.js8
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js13
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js15
-rw-r--r--spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js14
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js77
-rw-r--r--spec/frontend/merge_requests/components/target_project_dropdown_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js9
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js19
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js9
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js7
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js17
-rw-r--r--spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb37
-rw-r--r--spec/helpers/search_helper_spec.rb6
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb7
-rw-r--r--spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb121
-rw-r--r--spec/lib/gitlab/github_gists_import/representation/gist_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/page_counter_spec.rb12
-rw-r--r--spec/lib/gitlab/graphql/limit/field_call_count_spec.rb9
-rw-r--r--spec/models/badge_spec.rb4
-rw-r--r--spec/models/ci/build_spec.rb29
-rw-r--r--spec/models/ci/job_token/allowlist_spec.rb81
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb10
-rw-r--r--spec/models/ci/job_token/scope_spec.rb66
-rw-r--r--spec/models/ci/runner_namespace_spec.rb23
-rw-r--r--spec/presenters/search_service_presenter_spec.rb8
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb61
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb74
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb80
-rw-r--r--spec/requests/verifies_with_email_spec.rb10
-rw-r--r--spec/services/ci/create_pipeline_service/logger_spec.rb7
-rw-r--r--spec/services/projects/create_service_spec.rb21
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb15
-rw-r--r--spec/services/projects/import_service_spec.rb20
-rw-r--r--spec/services/search_service_spec.rb2
-rw-r--r--spec/support/helpers/project_template_test_helper.rb2
-rw-r--r--spec/support/helpers/service_desk_helper.rb9
-rw-r--r--spec/support/shared_contexts/models/ci/job_token_scope.rb21
-rw-r--r--spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb2
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/views/search/show.html.haml_spec.rb27
-rw-r--r--vendor/project_templates/typo3_distribution.tar.gzbin0 -> 71800 bytes
-rw-r--r--yarn.lock8
160 files changed, 2601 insertions, 892 deletions
diff --git a/.lefthook/pre-push/merge_conflicts b/.lefthook/pre-push/merge_conflicts
new file mode 100755
index 00000000000..26623d93095
--- /dev/null
+++ b/.lefthook/pre-push/merge_conflicts
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Adjusted from https://gitlab.com/fdegier/pre-push-hooks with hardcoded values for speed
+ORIGIN=origin
+DEFAULT_BRANCH=master
+
+if [[ -n "$ORIGIN" ]]
+then
+ # Pull the default branch from remote
+ git fetch --quiet origin "$DEFAULT_BRANCH":"$DEFAULT_BRANCH"
+fi
+
+# Check for merge conflicts and abort
+if git merge --autostash --no-commit --no-ff --no-edit "$DEFAULT_BRANCH" > /dev/null 2>&1
+then
+ # Able to merge without conflicts
+ git merge --abort > /dev/null 2>&1
+ exit 0
+else
+ echo "Merge conflicts detected when merging to $DEFAULT_BRANCH!"
+ git merge --abort > /dev/null 2>&1
+ exit 1
+fi
diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml
index 353ab9f721f..5afb7574ff1 100644
--- a/.rubocop_todo/gitlab/strong_memoize_attr.yml
+++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml
@@ -619,7 +619,6 @@ Gitlab/StrongMemoizeAttr:
- 'lib/gitlab/ci/pipeline/logger.rb'
- 'lib/gitlab/ci/pipeline/metrics.rb'
- 'lib/gitlab/ci/pipeline/quota/deployments.rb'
- - 'lib/gitlab/ci/pipeline/seed/pipeline.rb'
- 'lib/gitlab/ci/pipeline/seed/processable/resource_group.rb'
- 'lib/gitlab/ci/project_config/auto_devops.rb'
- 'lib/gitlab/ci/project_config/external_project.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1ab0a41532c..034cce40162 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-b3df64b5c2838a52aed21700299f7aa69b82c992
+52fb0853a49b14abddfc5a824d680bd255773d39
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
index b7bafe46327..f869d21d55f 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/base.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
@@ -5,14 +5,18 @@ 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 { NEW_BROADCAST_MESSAGE } from '../constants';
+import MessageForm from './message_form.vue';
import MessagesTable from './messages_table.vue';
const PER_PAGE = 20;
export default {
name: 'BroadcastMessagesBase',
+ NEW_BROADCAST_MESSAGE,
components: {
GlPagination,
+ MessageForm,
MessagesTable,
},
@@ -97,6 +101,7 @@ export default {
<template>
<div>
+ <message-form :broadcast-message="$options.NEW_BROADCAST_MESSAGE" />
<messages-table
v-if="hasVisibleMessages"
:messages="visibleMessages"
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
new file mode 100644
index 00000000000..07814ef2511
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlDatepicker, GlFormInput } from '@gitlab/ui';
+import { dateToTimeInputValue, timeToHoursMinutes } from '~/lib/utils/datetime/date_format_utility';
+
+export default {
+ name: 'DatetimePicker',
+ components: {
+ GlDatepicker,
+ GlFormInput,
+ },
+ props: {
+ value: {
+ type: Date,
+ required: true,
+ },
+ },
+ computed: {
+ date: {
+ get() {
+ return this.value;
+ },
+ set(val) {
+ const dup = new Date(this.value.getTime());
+ dup.setFullYear(val.getFullYear(), val.getMonth(), val.getDate());
+ this.$emit('input', dup);
+ },
+ },
+ time: {
+ get() {
+ return dateToTimeInputValue(this.value);
+ },
+ set(val) {
+ const dup = new Date(this.value.getTime());
+ const { hours, minutes } = timeToHoursMinutes(val);
+ dup.setHours(hours, minutes);
+ this.$emit('input', dup);
+ },
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center">
+ <gl-datepicker v-model="date" />
+ <gl-form-input v-model="time" size="sm" type="time" data-testid="time-picker" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
new file mode 100644
index 00000000000..36796708e78
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -0,0 +1,225 @@
+<script>
+import {
+ GlButton,
+ GlBroadcastMessage,
+ GlForm,
+ GlFormCheckbox,
+ GlFormCheckboxGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlFormText,
+ GlFormTextarea,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+import { createAlert, VARIANT_DANGER } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { BROADCAST_MESSAGES_PATH, THEMES, TYPES, TYPE_BANNER } from '../constants';
+import MessageFormGroup from './message_form_group.vue';
+import DatetimePicker from './datetime_picker.vue';
+
+const FORM_HEADERS = { headers: { 'Content-Type': 'application/json; charset=utf-8' } };
+
+export default {
+ name: 'MessageForm',
+ components: {
+ DatetimePicker,
+ GlButton,
+ GlBroadcastMessage,
+ GlForm,
+ GlFormCheckbox,
+ GlFormCheckboxGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlFormText,
+ GlFormTextarea,
+ MessageFormGroup,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['targetAccessLevelOptions'],
+ i18n: {
+ message: s__('BroadcastMessages|Message'),
+ messagePlaceholder: s__('BroadcastMessages|Your message here'),
+ type: s__('BroadcastMessages|Type'),
+ theme: s__('BroadcastMessages|Theme'),
+ dismissable: s__('BroadcastMessages|Dismissable'),
+ dismissableDescription: s__('BroadcastMessages|Allow users to dismiss the broadcast message'),
+ targetRoles: s__('BroadcastMessages|Target roles'),
+ targetRolesDescription: s__(
+ 'BroadcastMessages|The broadcast message displays only to users in projects and groups who have these roles.',
+ ),
+ targetPath: s__('BroadcastMessages|Target Path'),
+ targetPathDescription: s__('BroadcastMessages|Paths can contain wildcards, like */welcome'),
+ startsAt: s__('BroadcastMessages|Starts at'),
+ endsAt: s__('BroadcastMessages|Ends at'),
+ add: s__('BroadcastMessages|Add broadcast message'),
+ addError: s__('BroadcastMessages|There was an error adding broadcast message.'),
+ update: s__('BroadcastMessages|Update broadcast message'),
+ updateError: s__('BroadcastMessages|There was an error updating broadcast message.'),
+ },
+ messageThemes: THEMES,
+ messageTypes: TYPES,
+ props: {
+ broadcastMessage: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ message: this.broadcastMessage.message,
+ type: this.broadcastMessage.broadcastType,
+ theme: this.broadcastMessage.theme,
+ dismissable: this.broadcastMessage.dismissable || false,
+ targetPath: this.broadcastMessage.targetPath,
+ targetAccessLevels: this.broadcastMessage.targetAccessLevels,
+ targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
+ text,
+ value,
+ })),
+ startsAt: new Date(this.broadcastMessage.startsAt.getTime()),
+ endsAt: new Date(this.broadcastMessage.endsAt.getTime()),
+ };
+ },
+ computed: {
+ isBanner() {
+ return this.type === TYPE_BANNER;
+ },
+ messageBlank() {
+ return this.message.trim() === '';
+ },
+ messagePreview() {
+ return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.message;
+ },
+ isAddForm() {
+ return !this.broadcastMessage.id;
+ },
+ formPath() {
+ return this.isAddForm
+ ? BROADCAST_MESSAGES_PATH
+ : `${BROADCAST_MESSAGES_PATH}/${this.broadcastMessage.id}`;
+ },
+ formPayload() {
+ return JSON.stringify({
+ message: this.message,
+ broadcast_type: this.type,
+ theme: this.theme,
+ dismissable: this.dismissable,
+ target_path: this.targetPath,
+ target_access_levels: this.targetAccessLevels,
+ starts_at: this.startsAt.toISOString(),
+ ends_at: this.endsAt.toISOString(),
+ });
+ },
+ },
+ methods: {
+ async onSubmit() {
+ this.loading = true;
+
+ const success = await this.submitForm();
+ if (success) {
+ redirectTo(BROADCAST_MESSAGES_PATH);
+ } else {
+ this.loading = false;
+ }
+ },
+
+ async submitForm() {
+ const requestMethod = this.isAddForm ? 'post' : 'patch';
+
+ try {
+ await axios[requestMethod](this.formPath, this.formPayload, FORM_HEADERS);
+ } catch (e) {
+ const message = this.isAddForm
+ ? this.$options.i18n.addError
+ : this.$options.i18n.updateError;
+ createAlert({ message, variant: VARIANT_DANGER });
+ return false;
+ }
+ return true;
+ },
+ },
+};
+</script>
+<template>
+ <gl-form @submit.prevent="onSubmit">
+ <gl-broadcast-message class="gl-my-6" :type="type" :theme="theme" :dismissible="dismissable">
+ {{ messagePreview }}
+ </gl-broadcast-message>
+
+ <message-form-group :label="$options.i18n.message" label-for="message-textarea">
+ <gl-form-textarea
+ id="message-textarea"
+ v-model="message"
+ size="sm"
+ :placeholder="$options.i18n.messagePlaceholder"
+ />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.type" label-for="type-select">
+ <gl-form-select id="type-select" v-model="type" :options="$options.messageTypes" />
+ </message-form-group>
+
+ <template v-if="isBanner">
+ <message-form-group :label="$options.i18n.theme" label-for="theme-select">
+ <gl-form-select
+ id="theme-select"
+ v-model="theme"
+ :options="$options.messageThemes"
+ data-testid="theme-select"
+ />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.dismissable" label-for="dismissable-checkbox">
+ <gl-form-checkbox
+ id="dismissable-checkbox"
+ v-model="dismissable"
+ class="gl-mt-3"
+ data-testid="dismissable-checkbox"
+ >
+ <span>{{ $options.i18n.dismissableDescription }}</span>
+ </gl-form-checkbox>
+ </message-form-group>
+ </template>
+
+ <message-form-group
+ v-if="glFeatures.roleTargetedBroadcastMessages"
+ :label="$options.i18n.targetRoles"
+ data-testid="target-roles-checkboxes"
+ >
+ <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" />
+ <gl-form-text>
+ {{ $options.i18n.targetRolesDescription }}
+ </gl-form-text>
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.targetPath" label-for="target-path-input">
+ <gl-form-input id="target-path-input" v-model="targetPath" />
+ <gl-form-text>
+ {{ $options.i18n.targetPathDescription }}
+ </gl-form-text>
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.startsAt">
+ <datetime-picker v-model="startsAt" />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.endsAt">
+ <datetime-picker v-model="endsAt" />
+ </message-form-group>
+
+ <div class="form-actions gl-mb-3">
+ <gl-button
+ type="submit"
+ variant="confirm"
+ :loading="loading"
+ :disabled="messageBlank"
+ data-testid="submit-button"
+ >
+ {{ isAddForm ? $options.i18n.add : $options.i18n.update }}
+ </gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue
new file mode 100644
index 00000000000..eec51c0c28b
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlFormGroup } from '@gitlab/ui';
+
+export default {
+ name: 'MessageFormGroup',
+ components: {
+ GlFormGroup,
+ },
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ labelFor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group
+ :label="label"
+ :label-for="labelFor"
+ label-cols-sm="2"
+ label-class="gl-mt-3"
+ label-align-sm="right"
+ >
+ <slot></slot>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
index 44c6bc72705..a523dd3b391 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
@@ -2,6 +2,7 @@
import { GlButton, GlTableLite } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!';
@@ -78,6 +79,11 @@ export default {
safeHtmlConfig: {
ADD_TAGS: ['use'],
},
+ methods: {
+ formatDate(dateString) {
+ return formatDate(new Date(dateString));
+ },
+ },
};
</script>
<template>
@@ -91,6 +97,14 @@ export default {
<div v-safe-html:[$options.safeHtmlConfig]="preview"></div>
</template>
+ <template #cell(starts_at)="{ item: { starts_at } }">
+ {{ formatDate(starts_at) }}
+ </template>
+
+ <template #cell(ends_at)="{ item: { ends_at } }">
+ {{ formatDate(ends_at) }}
+ </template>
+
<template #cell(buttons)="{ item: { id, edit_path, disable_delete } }">
<gl-button
icon="pencil"
diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js
new file mode 100644
index 00000000000..6250d5a943d
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/constants.js
@@ -0,0 +1,35 @@
+import { s__ } from '~/locale';
+
+export const BROADCAST_MESSAGES_PATH = '/admin/broadcast_messages';
+
+export const TYPE_BANNER = 'banner';
+export const TYPE_NOTIFICATION = 'notification';
+
+export const TYPES = [
+ { value: TYPE_BANNER, text: s__('BroadcastMessages|Banner') },
+ { value: TYPE_NOTIFICATION, text: s__('BroadcastMessages|Notification') },
+];
+
+export const THEMES = [
+ { value: 'indigo', text: s__('BroadcastMessages|Indigo') },
+ { value: 'light-indigo', text: s__('BroadcastMessages|Light Indigo') },
+ { value: 'blue', text: s__('BroadcastMessages|Blue') },
+ { value: 'light-blue', text: s__('BroadcastMessages|Light Blue') },
+ { value: 'green', text: s__('BroadcastMessages|Green') },
+ { value: 'light-green', text: s__('BroadcastMessages|Light Green') },
+ { value: 'red', text: s__('BroadcastMessages|Red') },
+ { value: 'light-red', text: s__('BroadcastMessages|Light Red') },
+ { value: 'dark', text: s__('BroadcastMessages|Dark') },
+ { value: 'light', text: s__('BroadcastMessages|Light') },
+];
+
+export const NEW_BROADCAST_MESSAGE = {
+ message: '',
+ broadcastType: TYPES[0].value,
+ theme: THEMES[0].value,
+ dismissable: false,
+ targetPath: '',
+ targetAccessLevels: [],
+ startsAt: new Date(),
+ endsAt: new Date(),
+};
diff --git a/app/assets/javascripts/admin/broadcast_messages/edit.js b/app/assets/javascripts/admin/broadcast_messages/edit.js
new file mode 100644
index 00000000000..70a270f7a56
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/edit.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import MessageForm from './components/message_form.vue';
+
+export default () => {
+ const el = document.querySelector('#js-broadcast-message');
+ const {
+ id,
+ message,
+ broadcastType,
+ theme,
+ dismissable,
+ targetAccessLevels,
+ targetAccessLevelOptions,
+ targetPath,
+ startsAt,
+ endsAt,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'EditBroadcastMessage',
+ provide: {
+ targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions),
+ },
+ render(createElement) {
+ return createElement(MessageForm, {
+ props: {
+ broadcastMessage: {
+ id: parseInt(id, 10),
+ message,
+ broadcastType,
+ theme,
+ dismissable: dismissable === 'true',
+ targetAccessLevels: JSON.parse(targetAccessLevels),
+ targetPath,
+ startsAt: new Date(startsAt),
+ endsAt: new Date(endsAt),
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/broadcast_messages/index.js b/app/assets/javascripts/admin/broadcast_messages/index.js
index 81952d2033e..fd8b2aad4ec 100644
--- a/app/assets/javascripts/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/admin/broadcast_messages/index.js
@@ -3,11 +3,14 @@ import BroadcastMessagesBase from './components/base.vue';
export default () => {
const el = document.querySelector('#js-broadcast-messages');
- const { page, messagesCount, messages } = el.dataset;
+ const { page, targetAccessLevelOptions, messagesCount, messages } = el.dataset;
return new Vue({
el,
name: 'BroadcastMessages',
+ provide: {
+ targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions),
+ },
render(createElement) {
return createElement(BroadcastMessagesBase, {
props: {
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 5bc29750635..c95c90d5daf 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -6,6 +6,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert, VARIANT_INFO } from '~/flash';
import { s__, sprintf } from '~/locale';
import createEmptyBadge from '../empty_badge';
+import { PLACEHOLDERS } from '../constants';
import Badge from './badge.vue';
const badgePreviewDelayInMilliseconds = 1500;
@@ -50,9 +51,9 @@ export default {
return this.badgeInAddForm;
},
helpText() {
- const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
- .map((placeholder) => `<code>%{${placeholder}}</code>`)
- .join(', ');
+ const placeholders = PLACEHOLDERS.map((placeholder) => `<code>%{${placeholder}}</code>`).join(
+ ', ',
+ );
return sprintf(
s__('Badges|Supported %{docsLinkStart}variables%{docsLinkEnd}: %{placeholders}'),
{
diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js
index 8fbe3db5ef1..709436abca6 100644
--- a/app/assets/javascripts/badges/constants.js
+++ b/app/assets/javascripts/badges/constants.js
@@ -1,2 +1,10 @@
export const GROUP_BADGE = 'group';
export const PROJECT_BADGE = 'project';
+export const PLACEHOLDERS = [
+ 'project_path',
+ 'project_title',
+ 'project_name',
+ 'project_id',
+ 'default_branch',
+ 'commit_sha',
+];
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index c650a0627ca..3bd20dff9cc 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -23,6 +23,7 @@ import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
+import RunnerJobStatusBadge from '../components/runner_job_status_badge.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
@@ -48,6 +49,7 @@ export default {
RunnerPagination,
RunnerTypeTabs,
RunnerActionsCell,
+ RunnerJobStatusBadge,
},
mixins: [glFeatureFlagMixin()],
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
@@ -137,6 +139,12 @@ export default {
this.reportToSentry(error);
},
methods: {
+ jobsUrl(runner) {
+ const url = new URL(runner.adminUrl);
+ url.hash = '#/jobs';
+
+ return url.href;
+ },
onToggledPaused() {
// When a runner becomes Paused, the tab count can
// become stale, refetch outdated counts.
@@ -211,6 +219,12 @@ export default {
<runner-name :runner="runner" />
</gl-link>
</template>
+ <template #runner-job-status-badge="{ runner }">
+ <runner-job-status-badge
+ :href="jobsUrl(runner)"
+ :job-status="runner.jobExecutionStatus"
+ />
+ </template>
<template #runner-actions-cell="{ runner }">
<runner-actions-cell
:runner="runner"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index d5bc39ff039..4a72023b6a0 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -90,7 +90,9 @@ export default {
</div>
<div>
- <runner-job-status-badge :job-status="runner.jobExecutionStatus" />
+ <slot :runner="runner" name="runner-job-status-badge">
+ <runner-job-status-badge :job-status="runner.jobExecutionStatus" />
+ </slot>
<runner-summary-field icon="clock">
<gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL">
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
index 176fe57eebb..1e52acecfb8 100644
--- a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
@@ -44,6 +44,7 @@ export default {
<template>
<gl-badge
v-if="badge"
+ v-bind="$attrs"
size="sm"
class="gl-mr-3 gl-bg-transparent!"
variant="muted"
diff --git a/app/assets/javascripts/ci/runner/components/runner_list.vue b/app/assets/javascripts/ci/runner/components/runner_list.vue
index bc8cabad0fb..b2aad0aac4f 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list.vue
@@ -158,6 +158,9 @@ export default {
<template #runner-name="{ runner }">
<slot name="runner-name" :runner="runner" :index="index"></slot>
</template>
+ <template #runner-job-status-badge="{ runner }">
+ <slot name="runner-job-status-badge" :runner="runner" :index="index"></slot>
+ </template>
</runner-summary-cell>
</template>
diff --git a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
index 0e2d8821f36..dac807dceb0 100644
--- a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
@@ -1,5 +1,6 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __ } from '~/locale';
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
@@ -7,10 +8,7 @@ import getProjectQuery from '../graphql/queries/get_project.query.graphql';
export default {
BRANCHES_PER_PAGE,
components: {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlLoadingIcon,
+ GlCollapsibleListbox,
},
props: {
selectedProject: {
@@ -26,7 +24,6 @@ export default {
},
data() {
return {
- sourceBranchSearchQuery: '',
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
sourceBranchNames: [],
@@ -59,6 +56,9 @@ export default {
onSourceBranchSelect(branchName) {
this.$emit('change', branchName);
},
+ onSearch: debounce(function debouncedSearch(branchSearchQuery) {
+ this.onSourceBranchSearchQuery(branchSearchQuery);
+ }, 250),
onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
this.fetchSourceBranchNames({
@@ -83,7 +83,10 @@ export default {
});
const { branchNames, rootRef } = data?.project.repository || {};
- this.sourceBranchNames = branchNames || [];
+ this.sourceBranchNames =
+ branchNames.map((value) => {
+ return { text: value, value };
+ }) || [];
// Use root ref as the default selection
if (rootRef && !this.hasSelectedSourceBranch) {
@@ -102,33 +105,15 @@ export default {
</script>
<template>
- <gl-dropdown
- :text="branchDropdownText"
- :loading="initialSourceBranchNamesLoading"
- :disabled="!hasSelectedProject"
+ <gl-collapsible-listbox
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
- >
- <template #header>
- <gl-search-box-by-type
- :debounce="250"
- :value="sourceBranchSearchQuery"
- @input="onSourceBranchSearchQuery"
- />
- </template>
-
- <gl-loading-icon v-show="sourceBranchNamesLoading" />
- <template v-if="!sourceBranchNamesLoading">
- <gl-dropdown-item
- v-for="branchName in sourceBranchNames"
- v-show="!sourceBranchNamesLoading"
- :key="branchName"
- :is-checked="branchName === selectedBranchName"
- is-check-item
- class="gl-font-monospace"
- @click="onSourceBranchSelect(branchName)"
- >
- {{ branchName }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
+ :disabled="!hasSelectedProject"
+ :items="sourceBranchNames"
+ :loading="initialSourceBranchNamesLoading"
+ :searchable="true"
+ :searching="sourceBranchNamesLoading"
+ :toggle-text="branchDropdownText"
+ @search="onSearch"
+ @select="onSourceBranchSelect"
+ />
</template>
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
new file mode 100644
index 00000000000..25036984082
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
@@ -0,0 +1,8 @@
+import initEditBroadcastMessage from '~/admin/broadcast_messages/edit';
+import initBroadcastMessagesForm from '../broadcast_message';
+
+if (gon.features.vueBroadcastMessages) {
+ initEditBroadcastMessage();
+} else {
+ initBroadcastMessagesForm();
+}
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
index ffd976be8c6..1f37df2b340 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
@@ -1,6 +1,6 @@
import initBroadcastMessages from '~/admin/broadcast_messages';
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-import initBroadcastMessagesForm from './broadcast_message';
+import initBroadcastMessagesForm from '../broadcast_message';
if (gon.features.vueBroadcastMessages) {
initBroadcastMessages();
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
index 7ee5ec48f44..387b01aee7e 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
@@ -70,7 +70,6 @@ export default {
axios
.post(`${this.link}.json`)
.then(() => {
- this.isDisabled = false;
this.isLoading = false;
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
index efa2b3a4fff..51b46f25048 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
@@ -137,9 +137,6 @@ export default {
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
- pipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
},
};
</script>
@@ -184,7 +181,6 @@ export default {
:link="status.action.path"
:action-icon="status.action.icon"
data-qa-selector="action_button"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
index 993fa121d89..92f627b9d12 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
@@ -56,11 +56,6 @@ export default {
return Boolean(this.downstreamPipelines.length);
},
},
- methods: {
- onPipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
};
</script>
<template>
@@ -84,7 +79,6 @@ export default {
:update-dropdown="updateDropdown"
:stages-class="stagesClass"
data-testid="pipeline-stages"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="$emit('miniGraphStageClick')"
/>
<gl-icon
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
index 93110c8e3db..ec42b738e03 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
@@ -100,13 +100,6 @@ export default {
});
});
},
- pipelineActionRequestComplete() {
- // close the dropdown in MR widget
- this.$refs.dropdown.hide();
-
- // warn the pipelines table to update
- this.$emit('pipelineActionRequestComplete');
- },
stageAriaLabel(title) {
return sprintf(__('View Stage: %{title}'), { title });
},
@@ -158,7 +151,6 @@ export default {
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
<template v-if="isMergeTrain">
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
index e965dc5e6b0..ceb771132dd 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
@@ -28,11 +28,6 @@ export default {
default: false,
},
},
- methods: {
- onPipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
};
</script>
<template>
@@ -47,7 +42,6 @@ export default {
:stage="stage"
:update-dropdown="updateDropdown"
:is-merge-train="isMergeTrain"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="$emit('miniGraphStageClick')"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index f6e46c090d3..346f5735576 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -124,9 +124,6 @@ export default {
eventHub.$emit('postAction', this.endpoint);
this.cancelingPipeline = this.pipelineId;
},
- onPipelineActionRequestComplete() {
- eventHub.$emit('refreshPipelinesTable');
- },
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
},
@@ -179,7 +176,6 @@ export default {
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
:upstream-pipeline="item.triggered_by"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="trackPipelineMiniGraph"
/>
</template>
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 9602ca1ba88..07551c2342f 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -55,7 +55,6 @@ export default {
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
- eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
@@ -63,7 +62,6 @@ export default {
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
- eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
destroyed() {
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 497d8acc4a9..a44855c14d5 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -117,4 +117,8 @@ export default {
text: s__('ProjectTemplates|Pages/Bridgetown'),
icon: '.template-option .icon-gitlab_logo',
},
+ typo3_distribution: {
+ text: s__('ProjectTemplates|TYPO3 Distribution'),
+ icon: '.template-option .icon-typo3',
+ },
};
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 1d295e18332..e9214e3acff 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -2,11 +2,12 @@ import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
-import { escapeFileUrl } from '~/lib/utils/url_utility';
+import { escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import createStore from '~/code_navigation/store';
+import RefSelector from '~/ref/components/ref_selector.vue';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
@@ -20,6 +21,7 @@ import refsQuery from './queries/ref.query.graphql';
import createRouter from './router';
import { updateFormAction } from './utils/dom';
import { setTitle } from './utils/title';
+import { generateRefDestinationPath } from './utils/ref_switcher_utils';
Vue.use(Vuex);
Vue.use(PerformancePlugin, {
@@ -89,9 +91,34 @@ export default function setupVueRepositoryList() {
},
});
- initLastCommitApp();
+ const initRefSwitcher = () => {
+ const refSwitcherEl = document.getElementById('js-tree-ref-switcher');
+
+ if (!refSwitcherEl) return false;
+
+ const { projectId, projectRootPath } = refSwitcherEl.dataset;
+
+ return new Vue({
+ el: refSwitcherEl,
+ render(createElement) {
+ return createElement(RefSelector, {
+ props: {
+ projectId,
+ value: ref,
+ },
+ on: {
+ input(selectedRef) {
+ visitUrl(generateRefDestinationPath(projectRootPath, selectedRef));
+ },
+ },
+ });
+ },
+ });
+ };
+ initLastCommitApp();
initBlobControlsApp();
+ initRefSwitcher();
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
diff --git a/app/assets/javascripts/repository/utils/ref_switcher_utils.js b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
new file mode 100644
index 00000000000..8ff52104c93
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
@@ -0,0 +1,30 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+
+/**
+ * Matches the namespace and target directory/blob in a path
+ * Example: /root/Flight/-/blob/fix/main/test/spec/utils_spec.js
+ * Group 1: /-/blob
+ * Group 2: blob
+ * Group 3: main/test/spec/utils_spec.js
+ */
+const NAMESPACE_TARGET_REGEX = /(\/-\/(blob|tree))\/.*?\/(.*)/;
+
+/**
+ * Generates a ref destination path based on the selected ref and current path.
+ * A user could either be in the project root, a directory on the blob view.
+ * @param {string} projectRootPath - The root path for a project.
+ * @param {string} selectedRef - The selected ref from the ref dropdown.
+ */
+export function generateRefDestinationPath(projectRootPath, selectedRef) {
+ const currentPath = window.location.pathname;
+ let namespace = '/-/tree';
+ let target;
+ const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
+ if (match) {
+ [, namespace, , target] = match;
+ }
+
+ const destinationPath = joinPaths(projectRootPath, namespace, selectedRef, target);
+
+ return `${destinationPath}${window.location.hash}`;
+}
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 5fed66f88fa..6803d609dbc 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -369,10 +369,10 @@ export default {
<alert-details-table :alert="alert" :loading="loading" :statuses="statuses" />
</gl-tab>
- <metric-images-tab
- :data-testid="$options.tabsConfig[1].id"
- :title="$options.tabsConfig[1].title"
- />
+ <gl-tab :title="$options.tabsConfig[1].title">
+ <metric-images-tab :data-testid="$options.tabsConfig[1].id" />
+ </gl-tab>
+
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
<div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
<ul class="notes main-notes-list timeline">
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 74f14dbd5c9..9288cf7a733 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -337,6 +337,11 @@ export default {
this.select(KEY_WEB_IDE);
},
+ dismissCalloutOnActionClicked(dismiss) {
+ if (this.displayVscodeWebIdeCallout) {
+ dismiss();
+ }
+ },
},
webIdeButtonId: 'web-ide-link',
PREFERRED_EDITOR_KEY,
@@ -355,7 +360,7 @@ export default {
:category="isBlob ? 'primary' : 'secondary'"
:show-action-tooltip="!displayVscodeWebIdeCallout || !shouldShowCallout"
@select="select"
- @actionClicked="dismiss"
+ @actionClicked="dismissCalloutOnActionClicked(dismiss)"
/>
<local-storage-sync
:storage-key="$options.PREFERRED_EDITOR_KEY"
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 02e6d3a4551..4950561bcb7 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -636,13 +636,6 @@ $tabs-holder-z-index: 250;
margin: 3px 0;
}
- .ci-status-icon svg {
- margin: 3px 0;
- position: relative;
- overflow: visible;
- display: block;
- }
-
.normal {
flex: 1;
flex-basis: auto;
diff --git a/app/controllers/concerns/verifies_with_email.rb b/app/controllers/concerns/verifies_with_email.rb
index ac1475597ff..dd6560d6e9a 100644
--- a/app/controllers/concerns/verifies_with_email.rb
+++ b/app/controllers/concerns/verifies_with_email.rb
@@ -28,7 +28,7 @@ module VerifiesWithEmail
if user.unlock_token
# Prompt for the token if it already has been set
prompt_for_email_verification(user)
- elsif user.access_locked? || !AuthenticationEvent.initial_login_or_known_ip_address?(user, request.ip)
+ elsif user.access_locked? || !trusted_ip_address?(user)
# require email verification if:
# - their account has been locked because of too many failed login attempts, or
# - they have logged in before, but never from the current ip address
@@ -133,6 +133,12 @@ module VerifiesWithEmail
sign_in(user)
end
+ def trusted_ip_address?(user)
+ return true if Feature.disabled?(:check_ip_address_for_email_verification)
+
+ AuthenticationEvent.initial_login_or_known_ip_address?(user, request.ip)
+ end
+
def prompt_for_email_verification(user)
session[:verification_user_id] = user.id
self.resource = user
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index fbbfb9f23c5..66968b34380 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -47,6 +47,7 @@ class SearchController < ApplicationController
def show
@project = search_service.project
@group = search_service.group
+ @search_service = Gitlab::View::Presenter::Factory.new(search_service, current_user: current_user).fabricate!
return unless search_term_valid?
@@ -55,15 +56,11 @@ class SearchController < ApplicationController
@search_term = params[:search]
@sort = params[:sort] || default_sort
- @search_service = Gitlab::View::Presenter::Factory.new(search_service, current_user: current_user).fabricate!
-
@search_level = @search_service.level
@search_type = search_type
@global_search_duration_s = Benchmark.realtime do
@scope = @search_service.scope
- @without_count = @search_service.without_count?
- @show_snippets = @search_service.show_snippets?
@search_results = @search_service.search_results
@search_objects = @search_service.search_objects
@search_highlight = @search_service.search_highlight
diff --git a/app/graphql/mutations/ci/pipeline_schedule/play.rb b/app/graphql/mutations/ci/pipeline_schedule/play.rb
new file mode 100644
index 00000000000..056890852c9
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_schedule/play.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module PipelineSchedule
+ class Play < Base
+ graphql_name 'PipelineSchedulePlay'
+
+ authorize :play_pipeline_schedule
+
+ field :pipeline_schedule,
+ Types::Ci::PipelineScheduleType,
+ null: true,
+ description: 'Pipeline schedule after mutation.'
+
+ def resolve(id:)
+ schedule = authorized_find!(id: id)
+
+ job_id = ::Ci::PipelineScheduleService
+ .new(schedule.project, current_user)
+ .execute(schedule)
+
+ if job_id
+ { pipeline_schedule: schedule, errors: [] }
+ else
+ { pipeline_schedule: nil, errors: ['Unable to schedule a pipeline to run immediately.'] }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runner_groups_resolver.rb b/app/graphql/resolvers/ci/runner_groups_resolver.rb
new file mode 100644
index 00000000000..e28b532ebb3
--- /dev/null
+++ b/app/graphql/resolvers/ci/runner_groups_resolver.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class RunnerGroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include LooksAhead
+ include ResolvesGroups
+
+ type Types::GroupConnection, null: true
+ authorize :read_runner
+ authorizes_object!
+
+ alias_method :runner, :object
+
+ def resolve_with_lookahead(**args)
+ return unless runner.group_type?
+
+ BatchLoader::GraphQL.for(runner.id).batch(key: :runner_namespaces) do |runner_ids, loader|
+ plucked_runner_and_namespace_ids =
+ ::Ci::RunnerNamespace
+ .for_runner(runner_ids)
+ .select(:runner_id, :namespace_id)
+ .pluck(:runner_id, :namespace_id) # rubocop: disable CodeReuse/ActiveRecord)
+
+ namespace_ids = plucked_runner_and_namespace_ids.collect(&:last).uniq
+ groups = apply_lookahead(::Group.id_in(namespace_ids))
+ Preloaders::GroupPolicyPreloader.new(groups, current_user).execute
+ groups_by_id = groups.index_by(&:id)
+
+ runner_group_ids_by_runner_id =
+ plucked_runner_and_namespace_ids
+ .group_by { |runner_id, _namespace_id| runner_id }
+ .transform_values { |values| values.filter_map { |_runner_id, namespace_id| groups_by_id[namespace_id] } }
+
+ runner_ids.each do |runner_id|
+ runner_namespaces = runner_group_ids_by_runner_id[runner_id] || []
+
+ loader.call(runner_id, runner_namespaces)
+ end
+ end
+ end
+
+ private
+
+ def preloads
+ super.merge({ web_url: [:route] })
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
index 910b088d780..e75e9b81792 100644
--- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
@@ -29,7 +29,14 @@ module Resolvers
{
previous_stage_jobs_or_needs: [:needs, :pipeline],
artifacts: [:job_artifacts],
- pipeline: [:user]
+ pipeline: [:user],
+ detailed_status: [
+ :metadata,
+ { project: [:route, { namespace: :route }] }
+ ],
+ commit_path: [:pipeline, { project: [:route, { namespace: [:route] }] }],
+ short_sha: [:pipeline],
+ tags: [:tags]
}
end
end
diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
index da8fab93619..f4e044b81c9 100644
--- a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
@@ -13,20 +13,22 @@ module Resolvers
resolve_owner
end
- def preloads
- {
- full_path: [:route]
- }
- end
-
private
- def filtered_preloads
- selection = lookahead
+ def node_selection(selection = lookahead)
+ # There are no nodes or edges selections in RunnerOwnerProjectResolver, but rather a project directly
+ selection
+ end
+
+ def unconditional_includes
+ [:project_feature]
+ end
- preloads.each.flat_map do |name, requirements|
- selection&.selects?(name) ? requirements : []
- end
+ def preloads
+ {
+ full_path: [:route, { namespace: [:route] }],
+ web_url: [:route, { namespace: [:route] }]
+ }
end
def resolve_owner
@@ -48,7 +50,7 @@ module Resolvers
.transform_values { |runner_projects| runner_projects.first.project_id }
project_ids = owner_project_id_by_runner_id.values.uniq
- projects = Project.where(id: project_ids)
+ projects = apply_lookahead(Project.id_in(project_ids))
Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute
projects_by_id = projects.index_by(&:id)
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index 07da54adde3..6d136e9861c 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -38,8 +38,10 @@ module Types
field :executor_name, GraphQL::Types::String, null: true,
description: 'Executor last advertised by the runner.',
method: :executor_name
- field :groups, 'Types::GroupConnection', null: true,
- description: 'Groups the runner is associated with. For group runners only.'
+ field :groups, 'Types::GroupConnection',
+ null: true,
+ resolver: ::Resolvers::Ci::RunnerGroupsResolver,
+ description: 'Groups the runner is associated with. For group runners only.'
field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
description: 'ID of the runner.'
field :ip_address, GraphQL::Types::String, null: true,
@@ -116,14 +118,13 @@ module Types
Gitlab::Routing.url_helpers.edit_admin_runner_url(runner) if can_admin_runners?
end
- # rubocop: disable CodeReuse/ActiveRecord
def project_count
BatchLoader::GraphQL.for(runner.id).batch(key: :runner_project_count) do |ids, loader, args|
counts = ::Ci::Runner.project_type
.select(:id, 'COUNT(ci_runner_projects.id) as count')
.left_outer_joins(:runner_projects)
.id_in(ids)
- .group(:id)
+ .group(:id) # rubocop: disable CodeReuse/ActiveRecord
.index_by(&:id)
ids.each do |id|
@@ -131,13 +132,6 @@ module Types
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def groups
- return unless runner.group_type?
-
- batched_owners(::Ci::RunnerNamespace, Group, :runner_groups, :namespace_id)
- end
def job_execution_status
BatchLoader::GraphQL.for(runner.id).batch(key: :running_builds_exist) do |runner_ids, loader|
@@ -154,29 +148,6 @@ module Types
def can_admin_runners?
context[:current_user]&.can_admin_all_resources?
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def batched_owners(runner_assoc_type, assoc_type, key, column_name)
- BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader|
- plucked_runner_and_owner_ids = runner_assoc_type
- .select(:runner_id, column_name)
- .where(runner_id: runner_ids)
- .pluck(:runner_id, column_name)
- # In plucked_runner_and_owner_ids, first() represents the runner ID, and second() the owner ID,
- # so let's group the owner IDs by runner ID
- runner_owner_ids_by_runner_id = plucked_runner_and_owner_ids
- .group_by(&:first)
- .transform_values { |runner_and_owner_id| runner_and_owner_id.map(&:second) }
-
- owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq
- owners = assoc_type.where(id: owner_ids).index_by(&:id)
-
- runner_ids.each do |runner_id|
- loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || [])
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 1a44093ff58..0d36bfcf0f3 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -118,6 +118,7 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Retry
mount_mutation Mutations::Ci::PipelineSchedule::Delete
mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership
+ mount_mutation Mutations::Ci::PipelineSchedule::Play
mount_mutation Mutations::Ci::CiCdSettingsUpdate, deprecated: {
reason: :renamed,
replacement: 'ProjectCiCdSettingsUpdate',
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 1a9ac8d8206..e03365ad5f1 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -477,7 +477,7 @@ module SearchHelper
notes: { sort: 8, label: _("Comments"), condition: project_search_tabs?(:notes) || search_service.show_elasticsearch_tabs? },
milestones: { sort: 9, label: _("Milestones"), condition: project_search_tabs?(:milestones) || @project.nil? },
users: { sort: 10, label: _("Users"), condition: show_user_search_tab? },
- snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? }
+ snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: search_service.show_snippets? && @project.nil? }
}
end
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 4339d419b48..0676de10d02 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -8,6 +8,8 @@ class Badge < ApplicationRecord
# the placeholder is found.
PLACEHOLDERS = {
'project_path' => :full_path,
+ 'project_title' => :title,
+ 'project_name' => :path,
'project_id' => :id,
'default_branch' => :default_branch,
'commit_sha' => ->(project) { project.commit&.sha }
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 34b5b637422..7f42b21bc87 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1135,6 +1135,17 @@ module Ci
end
end
+ def partition_id_token_prefix
+ partition_id.to_s(16) if Feature.enabled?(:ci_build_partition_id_token_prefix, project)
+ end
+
+ override :format_token
+ def format_token(token)
+ return token if partition_id_token_prefix.nil?
+
+ "#{partition_id_token_prefix}_#{token}"
+ end
+
protected
def run_status_commit_hooks!
diff --git a/app/models/ci/job_token/allowlist.rb b/app/models/ci/job_token/allowlist.rb
new file mode 100644
index 00000000000..9e9a0a68ebd
--- /dev/null
+++ b/app/models/ci/job_token/allowlist.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+module Ci
+ module JobToken
+ class Allowlist
+ def initialize(source_project, direction:)
+ @source_project = source_project
+ @direction = direction
+ end
+
+ def includes?(target_project)
+ source_links
+ .with_target(target_project)
+ .exists?
+ end
+
+ def projects
+ Project.from_union(target_projects, remove_duplicates: false)
+ end
+
+ private
+
+ def source_links
+ Ci::JobToken::ProjectScopeLink
+ .with_source(@source_project)
+ .where(direction: @direction)
+ end
+
+ def target_project_ids
+ source_links
+ # pluck needed to avoid ci and main db join
+ .pluck(:target_project_id)
+ end
+
+ def target_projects
+ [
+ Project.id_in(@source_project),
+ Project.id_in(target_project_ids)
+ ]
+ end
+ end
+ end
+end
diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb
index 3fdf07123e6..b784f93651a 100644
--- a/app/models/ci/job_token/project_scope_link.rb
+++ b/app/models/ci/job_token/project_scope_link.rb
@@ -12,8 +12,8 @@ module Ci
belongs_to :target_project, class_name: 'Project'
belongs_to :added_by, class_name: 'User'
- scope :from_project, ->(project) { where(source_project: project) }
- scope :to_project, ->(project) { where(target_project: project) }
+ scope :with_source, ->(project) { where(source_project: project) }
+ scope :with_target, ->(project) { where(target_project: project) }
validates :source_project, presence: true
validates :target_project, presence: true
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index 1aa49b95201..e320c0f92d1 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -1,49 +1,58 @@
# frozen_string_literal: true
-# This model represents the surface where a CI_JOB_TOKEN can be used.
-# A Scope is initialized with the project that the job token belongs to,
-# and indicates what are all the other projects that the token could access.
+# This model represents the scope of access for a CI_JOB_TOKEN.
#
-# By default a job token can only access its own project, which is the same
-# project that defines the scope.
-# By adding ScopeLinks to the scope we can allow other projects to be accessed
-# by the job token. This works as an allowlist of projects for a job token.
+# A scope is initialized with a project.
+#
+# Projects can be added to the scope by adding ScopeLinks to
+# create an allowlist of projects in either access direction (inbound, outbound).
+#
+# Currently, projects in the outbound allowlist can be accessed via the token
+# in the source project.
+#
+# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access
+# the source project.
+#
+# CI_JOB_TOKEN should be considered untrusted without these features enabled.
#
-# If a project is not included in the scope we should not allow the job user
-# to access it since operations using CI_JOB_TOKEN should be considered untrusted.
module Ci
module JobToken
class Scope
- attr_reader :source_project
+ attr_reader :current_project
- def initialize(project)
- @source_project = project
+ def initialize(current_project)
+ @current_project = current_project
end
- def includes?(target_project)
- # if the setting is disabled any project is considered to be in scope.
- return true unless source_project.ci_outbound_job_token_scope_enabled?
+ def allows?(accessed_project)
+ self_referential?(accessed_project) || outbound_allows?(accessed_project)
+ end
- target_project.id == source_project.id ||
- Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists?
+ def outbound_projects
+ outbound_allowlist.projects
end
+ # Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project
def all_projects
- Project.from_union(target_projects, remove_duplicates: false)
+ outbound_projects
end
private
- def target_project_ids
- Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
+ def outbound_allows?(accessed_project)
+ # if the setting is disabled any project is considered to be in scope.
+ return true unless @current_project.ci_outbound_job_token_scope_enabled?
+
+ outbound_allowlist.includes?(accessed_project)
+ end
+
+ def outbound_allowlist
+ Ci::JobToken::Allowlist.new(@current_project, direction: :outbound)
end
- def target_projects
- [
- Project.id_in(source_project),
- Project.id_in(target_project_ids)
- ]
+ def self_referential?(accessed_project)
+ @current_project.id == accessed_project.id
end
end
end
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index 82390ccc538..502ceae3675 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -15,6 +15,8 @@ module Ci
validates :runner_id, uniqueness: { scope: :namespace_id }
validate :group_runner_type
+ scope :for_runner, ->(runner_id) { where(runner_id: runner_id) }
+
def recent_runners
::Ci::Runner.belonging_to_group(namespace_id).recent
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 5cd80418a14..afc31e38e81 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -846,6 +846,7 @@ class Group < Namespace
def has_project_with_service_desk_enabled?
Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
+ strong_memoize_attr :has_project_with_service_desk_enabled?, :has_project_with_service_desk_enabled
def activity_path
Gitlab::Routing.url_helpers.activity_group_path(self)
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 74d0c88674e..aaa44b2ff54 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -121,7 +121,7 @@ class ProjectPolicy < BasePolicy
desc "If user is authenticated via CI job token then the target project should be in scope"
condition(:project_allowed_for_job_token) do
- !@user&.from_ci_job_token? || @user.ci_job_token_scope.includes?(project)
+ !@user&.from_ci_job_token? || @user.ci_job_token_scope.allows?(project)
end
with_scope :subject
diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb
index 7994957d50c..d7d959217b0 100644
--- a/app/presenters/search_service_presenter.rb
+++ b/app/presenters/search_service_presenter.rb
@@ -45,4 +45,10 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
def without_count?
search_objects.is_a?(Kaminari::PaginatableWithoutCount)
end
+
+ def advanced_search_enabled?
+ false
+ end
end
+
+SearchServicePresenter.prepend_mod_with('SearchServicePresenter')
diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb
index 536eaa56f9b..d320382d19f 100644
--- a/app/services/ci/pipeline_schedule_service.rb
+++ b/app/services/ci/pipeline_schedule_service.rb
@@ -8,7 +8,7 @@ module Ci
# Ensure `next_run_at` is set properly before creating a pipeline.
# Otherwise, multiple pipelines could be created in a short interval.
schedule.schedule_next_run!
- RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner&.id)
+ RunPipelineScheduleWorker.perform_async(schedule.id, current_user&.id)
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index c72f9b4b602..a4b473f35c6 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -317,6 +317,3 @@ module Projects
end
Projects::CreateService.prepend_mod_with('Projects::CreateService')
-
-# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::CreateService as well
-Projects::CreateService.prepend(Measurable)
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index ddbcfbb675c..a1f55f547a1 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -3,8 +3,6 @@
module Projects
module ImportExport
class ExportService < BaseService
- prepend Measurable
-
def initialize(*args)
super
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 6a13b8e38c1..967a1e990b2 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -179,6 +179,3 @@ module Projects
end
Projects::ImportService.prepend_mod_with('Projects::ImportService')
-
-# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::ImportService as well
-Projects::ImportService.prepend(Measurable)
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index f38522b9764..403a2f077b0 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -45,9 +45,9 @@ class SearchService
end
def show_snippets?
- return @show_snippets if defined?(@show_snippets)
-
- @show_snippets = params[:snippets] == 'true'
+ strong_memoize(:show_snippets) do
+ params[:snippets] == 'true'
+ end
end
delegate :scope, to: :search_service
diff --git a/app/views/admin/broadcast_messages/edit.html.haml b/app/views/admin/broadcast_messages/edit.html.haml
index 569aaa29cc4..28301833f7d 100644
--- a/app/views/admin/broadcast_messages/edit.html.haml
+++ b/app/views/admin/broadcast_messages/edit.html.haml
@@ -1,4 +1,19 @@
- breadcrumb_title _("Messages")
- page_title _("Broadcast Messages")
+- vue_app_enabled = Feature.enabled?(:vue_broadcast_messages, current_user)
-= render 'form'
+- if vue_app_enabled
+ #js-broadcast-message{ data: {
+ id: @broadcast_message.id,
+ message: @broadcast_message.message,
+ broadcast_type: @broadcast_message.broadcast_type,
+ theme: @broadcast_message.theme,
+ dismissable: @broadcast_message.dismissable.to_s,
+ target_access_levels: @broadcast_message.target_access_levels,
+ target_path: @broadcast_message.target_path,
+ starts_at: @broadcast_message.starts_at,
+ ends_at: @broadcast_message.ends_at,
+ target_access_level_options: target_access_level_options.to_json,
+ } }
+- else
+ = render 'form'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 7559365e49a..7a005f9c982 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -10,6 +10,7 @@
- if vue_app_enabled
#js-broadcast-messages{ data: {
page: params[:page] || 1,
+ target_access_level_options: target_access_level_options.to_json,
messages_count: @broadcast_messages.total_count,
messages: @broadcast_messages.map { |message| {
id: message.id,
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index d4b1c3c27f1..a99d76f99a7 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -22,7 +22,9 @@
.form-actions
- if @milestone.new_record?
= f.submit _('Create milestone'), data: { qa_selector: "create_milestone_button" }, pajamas_button: true
- = link_to _("Cancel"), group_milestones_path(@group), class: "btn gl-button btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: group_milestones_path(@group)) do
+ = _("Cancel")
- else
= f.submit _('Update milestone'), pajamas_button: true
- = link_to _("Cancel"), group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: group_milestone_path(@group, @milestone)) do
+ = _("Cancel")
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index 096d2543502..b50b376fdf2 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -19,5 +19,8 @@
= link_to sprite_icon('question-o'), help_page_path('user/project/import/manifest')
.gl-mb-3
- = submit_tag _('List available repositories'), class: 'gl-button btn btn-confirm'
- = link_to _('Cancel'), new_project_path, class: 'gl-button btn btn-default btn-cancel'
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
+ = _('List available repositories')
+
+ = render Pajamas::ButtonComponent.new(href: new_project_path) do
+ = _('Cancel')
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 5748b4b0330..86238a41f0b 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -10,8 +10,8 @@
- base_domain_link_start = link_start % { url: base_domain_path }
- help_link_continouos = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
.row
.col-lg-12
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 29bdca1c876..fd807350245 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,7 +2,7 @@
.tree-ref-container.gl-display-flex.mb-2.mb-md-0
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'tree', show_create: true
+ #js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project) } }
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index c15afd7bd5b..3e483fe8cd2 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -23,7 +23,7 @@
= search_filter_link 'milestones', _("Milestones")
= users
- - elsif @show_snippets
+ - elsif @search_service.show_snippets?
= search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
- else
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index c58f492f633..e1efa271d57 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -9,7 +9,7 @@
- project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace)
- if @search_results
- - if @without_count
+ - if @search_service.without_count?
- page_description(_("%{scope} results for term '%{term}'") % { scope: @scope, term: @search_term })
- else
- page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index e3b208cd495..b47ddde097e 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -25,6 +25,7 @@
- cluster_cost_management
- code_quality
- code_review
+- code_search
- code_suggestions
- code_testing
- commerce_integrations
@@ -50,6 +51,7 @@
- design_system
- devops_reports
- disaster_recovery
+- dora_metrics
- dynamic_application_security_testing
- editor_extension
- environment_management
diff --git a/config/feature_flags/development/check_ip_address_for_email_verification.yml b/config/feature_flags/development/check_ip_address_for_email_verification.yml
new file mode 100644
index 00000000000..1d0c640d9a4
--- /dev/null
+++ b/config/feature_flags/development/check_ip_address_for_email_verification.yml
@@ -0,0 +1,8 @@
+---
+name: check_ip_address_for_email_verification
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106441
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385196
+milestone: "15.7"
+type: development
+group: group::anti-abuse
+default_enabled: false
diff --git a/config/feature_flags/development/ci_build_partition_id_token_prefix.yml b/config/feature_flags/development/ci_build_partition_id_token_prefix.yml
new file mode 100644
index 00000000000..5b3cd22a489
--- /dev/null
+++ b/config/feature_flags/development/ci_build_partition_id_token_prefix.yml
@@ -0,0 +1,8 @@
+---
+name: ci_build_partition_id_token_prefix
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106179
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385401
+milestone: '15.7'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml b/config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml
deleted file mode 100644
index e3ed761f97b..00000000000
--- a/config/feature_flags/ops/gitlab_service_measuring_projects_create_service.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: gitlab_service_measuring_projects_create_service
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30977
-rollout_issue_url:
-milestone: '13.0'
-type: ops
-group: group::application performance
-default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml b/config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml
deleted file mode 100644
index 0ce25441f25..00000000000
--- a/config/feature_flags/ops/gitlab_service_measuring_projects_import_export_export_service.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: gitlab_service_measuring_projects_import_export_export_service
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30977
-rollout_issue_url:
-milestone: '13.0'
-type: ops
-group: group::application performance
-default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml b/config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml
deleted file mode 100644
index aa01e15aef5..00000000000
--- a/config/feature_flags/ops/gitlab_service_measuring_projects_import_service.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: gitlab_service_measuring_projects_import_service
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30977
-rollout_issue_url:
-milestone: '13.0'
-type: ops
-group: group::application performance
-default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 682a974fb4d..b8ff927921c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4310,6 +4310,25 @@ Input type: `PipelineScheduleDeleteInput`
| <a id="mutationpipelinescheduledeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationpipelinescheduledeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.pipelineSchedulePlay`
+
+Input type: `PipelineSchedulePlayInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationpipelinescheduleplayclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationpipelinescheduleplayid"></a>`id` | [`CiPipelineScheduleID!`](#cipipelinescheduleid) | ID of the pipeline schedule to mutate. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationpipelinescheduleplayclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationpipelinescheduleplayerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationpipelinescheduleplaypipelineschedule"></a>`pipelineSchedule` | [`PipelineSchedule`](#pipelineschedule) | Pipeline schedule after mutation. |
+
### `Mutation.pipelineScheduleTakeOwnership`
Input type: `PipelineScheduleTakeOwnershipInput`
@@ -11093,7 +11112,7 @@ CI/CD variables for a project.
| <a id="cirunnerdescription"></a>`description` | [`String`](#string) | Description of the runner. |
| <a id="cirunnereditadminurl"></a>`editAdminUrl` | [`String`](#string) | Admin form URL of the runner. Only available for administrators. |
| <a id="cirunnerexecutorname"></a>`executorName` | [`String`](#string) | Executor last advertised by the runner. |
-| <a id="cirunnergroups"></a>`groups` | [`GroupConnection`](#groupconnection) | Groups the runner is associated with. For group runners only. (see [Connections](#connections)) |
+| <a id="cirunnergroups"></a>`groups` | [`GroupConnection`](#groupconnection) | Types::GroupConnection. (see [Connections](#connections)) |
| <a id="cirunnerid"></a>`id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. |
| <a id="cirunneripaddress"></a>`ipAddress` | [`String`](#string) | IP address of the runner. |
| <a id="cirunnerjobcount"></a>`jobCount` | [`Int`](#int) | Number of jobs processed by the runner (limited to 1000, plus one to indicate that more items exist). |
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
index cc99c137a47..14146745d86 100644
--- a/doc/api/group_badges.md
+++ b/doc/api/group_badges.md
@@ -15,6 +15,8 @@ Badges support placeholders that are replaced in real time in both the link and
<!-- vale gitlab.Spelling = NO -->
- **%{project_path}**: replaced by the project path.
+- **%{project_title}**: replaced by the project title.
+- **%{project_name}**: replaced by the project name.
- **%{project_id}**: replaced by the project ID.
- **%{default_branch}**: replaced by the project default branch.
- **%{commit_sha}**: replaced by the last project's commit SHA.
diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md
index d83aa370808..52d32ab17b9 100644
--- a/doc/api/project_badges.md
+++ b/doc/api/project_badges.md
@@ -13,6 +13,8 @@ Badges support placeholders that are replaced in real-time in both the link and
<!-- vale gitlab.Spelling = NO -->
- **%{project_path}**: Replaced by the project path.
+- **%{project_title}**: Replaced by the project title.
+- **%{project_name}**: Replaced by the project name.
- **%{project_id}**: Replaced by the project ID.
- **%{default_branch}**: Replaced by the project default branch.
- **%{commit_sha}**: Replaced by the last project's commit SHA.
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 3da4aeb5323..10dfa0174a0 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -775,7 +775,7 @@ You can configure [Auto DevOps](../../topics/autodevops/index.md) to pass CI/CD
to a running application.
To make a CI/CD variable available as an environment variable in the running application's container,
-[prefix the variable key](../../topics/autodevops/customize.md#application-secret-variables)
+[prefix the variable key](../../topics/autodevops/cicd_variables.md#configure-application-secret-variables)
with `K8S_SECRET_`.
CI/CD variables with multi-line values are not supported.
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index ac473b40477..190a6f6eda2 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -102,7 +102,7 @@ it's declared under the `reports:sast` key in the job definition, not because of
### Policies
-Certain GitLab workflows, such as [AutoDevOps](../../topics/autodevops/customize.md#disable-jobs),
+Certain GitLab workflows, such as [AutoDevOps](../../topics/autodevops/cicd_variables.md#job-disabling-variables),
define CI/CD variables to indicate that given scans should be disabled. You can check for this by looking
for variables such as:
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index d0a69028030..02983559fd6 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -170,17 +170,17 @@ For more information on:
### Configure SAML on your IdP
-When configuring a SAML app on the IdP, you need at least:
+To configure a SAML application on your IdP, you need at least the following information:
-- Assertion consumer service URL
-- Issuer
-- [`NameID`](../user/group/saml_sso/index.md#nameid)
-- [Email address claim](#configure-assertions)
+- Assertion consumer service URL.
+- Issuer.
+- [`NameID`](../user/group/saml_sso/index.md#nameid).
+- [Email address claim](#configure-assertions).
-For example configurations, see the [notes on specific providers](#set-up-identity-providers).
+For an example configuration, see [set up identity providers](#set-up-identity-providers).
-Your identity provider may require additional configuration.
-See [additional information on configuring a SAML app](#additional-configuration-for-saml-apps-on-your-idp) on your IdP for more information.
+Your IdP may need additional configuration. For more information, see
+[additional configuration for SAML apps on your IdP](#additional-configuration-for-saml-apps-on-your-idp).
### Configure GitLab to use multiple SAML IdPs
diff --git a/doc/topics/autodevops/cicd_variables.md b/doc/topics/autodevops/cicd_variables.md
new file mode 100644
index 00000000000..db2b052784d
--- /dev/null
+++ b/doc/topics/autodevops/cicd_variables.md
@@ -0,0 +1,331 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# CI/CD variables
+
+Use CI/CD variables to set up the Auto DevOps domain, provide a custom
+Helm chart, or scale your application.
+
+## Build and deployment variables
+
+Use these variables to customize and deploy your build.
+
+| **CI/CD variable** | **Description** |
+|-----------------------------------------|------------------------------------|
+| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the Ingress hosts. |
+| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the Ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. |
+| `AUTO_BUILD_IMAGE_VERSION` | Customize the image version used for the `build` job. See [list of versions](https://gitlab.com/gitlab-org/cluster-integration/auto-build-image/-/releases). |
+| `AUTO_DEPLOY_IMAGE_VERSION` | Customize the image version used for Kubernetes deployment jobs. See [list of versions](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/releases). |
+| `AUTO_DEVOPS_ATOMIC_RELEASE` | As of GitLab 13.0, Auto DevOps uses [`--atomic`](https://v2.helm.sh/docs/helm/#options-43) for Helm deployments by default. Set this variable to `false` to disable the use of `--atomic` |
+| `AUTO_DEVOPS_BUILD_IMAGE_CNB_ENABLED` | Set to `false` to use Herokuish instead of Cloud Native Buildpacks with Auto Build. [More details](stages.md#auto-build-using-cloud-native-buildpacks). |
+| `AUTO_DEVOPS_BUILD_IMAGE_CNB_BUILDER` | The builder used when building with Cloud Native Buildpacks. The default builder is `heroku/buildpacks:18`. [More details](stages.md#auto-build-using-cloud-native-buildpacks). |
+| `AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` | Extra arguments to be passed to the `docker build` command. Using quotes doesn't prevent word splitting. [More details](customize.md#passing-arguments-to-docker-build). |
+| `AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES` | A [comma-separated list of CI/CD variable names](customize.md#forward-cicd-variables-to-the-build-environment) to be forwarded to the build environment (the buildpack builder or `docker build`). |
+| `AUTO_DEVOPS_BUILD_IMAGE_CNB_PORT` | In GitLab 15.0 and later, port exposed by the generated Docker image. Set to `false` to prevent exposing any ports. Defaults to `5000`. |
+| `AUTO_DEVOPS_CHART` | Helm Chart used to deploy your apps. Defaults to the one [provided by GitLab](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/tree/master/assets/auto-deploy-app). |
+| `AUTO_DEVOPS_CHART_REPOSITORY` | Helm Chart repository used to search for charts. Defaults to `https://charts.gitlab.io`. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | Used to set the name of the Helm repository. Defaults to `gitlab`. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | Used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | Used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_PASS_CREDENTIALS` | From GitLab 14.2, set to a non-empty value to enable forwarding of the Helm repository credentials to the chart server when the chart artifacts are on a different host than repository. |
+| `AUTO_DEVOPS_COMMON_NAME` | From GitLab 15.5, set to a valid domain name to customize the common name used for the TLS certificate. Defaults to `le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN`. Set to `false` to not set this alternative host on the Ingress. |
+| `AUTO_DEVOPS_DEPLOY_DEBUG` | From GitLab 13.1, if this variable is present, Helm outputs debug logs. |
+| `AUTO_DEVOPS_ALLOW_TO_FORCE_DEPLOY_V<N>` | From [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image) v1.0.0, if this variable is present, a new major version of chart is forcibly deployed. For more information, see [Ignore warnings and continue deploying](upgrading_auto_deploy_dependencies.md#ignore-warnings-and-continue-deploying). |
+| `BUILDPACK_URL` | A full Buildpack URL. [Must point to a URL supported by Pack or Herokuish](customize.md#custom-buildpacks). |
+| `CANARY_ENABLED` | Used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
+| `BUILDPACK_VOLUMES` | Specify one or more [Buildpack volumes to mount](stages.md#mount-volumes-into-the-build-container). Use a pipe `|` as list separator. |
+| `CANARY_PRODUCTION_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. Takes precedence over `CANARY_REPLICAS`. Defaults to 1. |
+| `CANARY_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md). Defaults to 1. |
+| `CI_APPLICATION_REPOSITORY` | The repository of container image being built or deployed, `$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG`. For more details, read [Custom container image](customize.md#custom-container-image). |
+| `CI_APPLICATION_TAG` | The tag of the container image being built or deployed, `$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG`. For more details, read [Custom container image](customize.md#custom-container-image). |
+| `DAST_AUTO_DEPLOY_IMAGE_VERSION` | Customize the image version used for DAST deployments on the default branch. Should usually be the same as `AUTO_DEPLOY_IMAGE_VERSION`. See [list of versions](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/releases). |
+| `DOCKERFILE_PATH` | From GitLab 13.2, allows overriding the [default Dockerfile path for the build stage](customize.md#custom-dockerfile) |
+| `HELM_RELEASE_NAME` | From GitLab 12.1, allows the `helm` release name to be overridden. Can be used to assign unique release names when deploying multiple projects to a single namespace. |
+| `HELM_UPGRADE_VALUES_FILE` | From GitLab 12.6, allows the `helm upgrade` values file to be overridden. Defaults to `.gitlab/auto-deploy-values.yaml`. |
+| `HELM_UPGRADE_EXTRA_ARGS` | Allows extra options in `helm upgrade` commands when deploying the application. Using quotes doesn't prevent word splitting. |
+| `INCREMENTAL_ROLLOUT_MODE` | If present, can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. Set to `manual` for manual deployment jobs or `timed` for automatic rollout deployments with a 5 minute delay each one. |
+| `K8S_SECRET_*` | Any variable prefixed with [`K8S_SECRET_`](#configure-application-secret-variables) is made available by Auto DevOps as environment variables to the deployed application. |
+| `KUBE_CONTEXT` | From GitLab 14.5, can be used to select a context to use from `KUBECONFIG`. When `KUBE_CONTEXT` is blank, the default context in `KUBECONFIG` (if any) is used. A context must be selected when used [with the agent for Kubernetes](../../user/clusters/agent/ci_cd_workflow.md). |
+| `KUBE_INGRESS_BASE_DOMAIN` | Can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/gitlab_managed_clusters.md#base-domain) for more information. |
+| `KUBE_NAMESPACE` | The namespace used for deployments. When using certificate-based clusters, [this value should not be overwritten directly](../../user/project/clusters/deploy_to_cluster.md#custom-namespace). |
+| `KUBECONFIG` | The kubeconfig to use for deployments. User-provided values take priority over GitLab-provided values. |
+| `PRODUCTION_REPLICAS` | Number of replicas to deploy in the production environment. Takes precedence over `REPLICAS` and defaults to 1. For zero downtime upgrades, set to 2 or greater. |
+| `REPLICAS` | Number of replicas to deploy. Defaults to 1. Change this variable instead of [modifying](customize.md#customize-values-for-helm-chart) `replicaCount`. |
+| `ROLLOUT_RESOURCE_TYPE` | Allows specification of the resource type being deployed when using a custom Helm chart. Default value is `deployment`. |
+| `ROLLOUT_STATUS_DISABLED` | From GitLab 12.0, used to disable rollout status check because it does not support all resource types, for example, `cronjob`. |
+| `STAGING_ENABLED` | Used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
+| `TRACE` | Set to any value to make Helm commands produce verbose output. You can use this setting to help diagnose Auto DevOps deployment problems. |
+
+## Database variables
+
+Use these variables to integrate CI/CD with PostgreSQL databases.
+
+| **CI/CD variable** | **Description** |
+|-----------------------------------------|------------------------------------|
+| `DB_INITIALIZE` | Used to specify the command to run to initialize the application's PostgreSQL database. Runs inside the application pod. |
+| `DB_MIGRATE` | Used to specify the command to run to migrate the application's PostgreSQL database. Runs inside the application pod. |
+| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled. Defaults to `true`. Set to `false` to disable the automatic deployment of PostgreSQL. |
+| `POSTGRES_USER` | The PostgreSQL user. Defaults to `user`. Set it to use a custom username. |
+| `POSTGRES_PASSWORD` | The PostgreSQL password. Defaults to `testing-password`. Set it to use a custom password. |
+| `POSTGRES_DB` | The PostgreSQL database name. Defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/index.md#predefined-cicd-variables). Set it to use a custom database name. |
+| `POSTGRES_VERSION` | Tag for the [`postgres` Docker image](https://hub.docker.com/_/postgres) to use. Defaults to `9.6.16` for tests and deployments as of GitLab 13.0 (previously `9.6.2`). If `AUTO_DEVOPS_POSTGRES_CHANNEL` is set to `1`, deployments uses the default version `9.6.2`. |
+| `POSTGRES_HELM_UPGRADE_VALUES_FILE` | In GitLab 13.8 and later, and when using [auto-deploy-image v2](upgrading_auto_deploy_dependencies.md), this variable allows the `helm upgrade` values file for PostgreSQL to be overridden. Defaults to `.gitlab/auto-deploy-postgres-values.yaml`. |
+| `POSTGRES_HELM_UPGRADE_EXTRA_ARGS` | In GitLab 13.8 and later, and when using [auto-deploy-image v2](upgrading_auto_deploy_dependencies.md), this variable allows extra PostgreSQL options in `helm upgrade` commands when deploying the application. Note that using quotes doesn't prevent word splitting. |
+| `POSTGRES_CHART_REPOSITORY` | Helm Chart repository used to search for PostgreSQL chart. Defaults to `https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami`. |
+| `POSTGRES_CHART_VERSION` | Helm Chart version used for PostgreSQL chart. Defaults to `8.2.1`. |
+
+## Job-disabling variables
+
+Use these variables to disable CI/CD jobs.
+
+| **Job name** | **CI/CD variable** | **GitLab version** | **Description** |
+|----------------------------------------|---------------------------------|-----------------------|-----------------|
+| `.fuzz_base` | `COVFUZZ_DISABLED` | [From GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34984) | [Read more](../../user/application_security/coverage_fuzzing/index.md) about how `.fuzz_base` provide capability for your own jobs. If the variable is present, your jobs aren't created. |
+| `apifuzzer_fuzz` | `API_FUZZING_DISABLED` | [From GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39135) | If the variable is present, the job isn't created. |
+| `build` | `BUILD_DISABLED` | | If the variable is present, the job isn't created. |
+| `build_artifact` | `BUILD_DISABLED` | | If the variable is present, the job isn't created. |
+| `bandit-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `brakeman-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `canary` | `CANARY_ENABLED` | | This manual job is created if the variable is present. |
+| `cluster_image_scanning` | `CLUSTER_IMAGE_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `code_intelligence` | `CODE_INTELLIGENCE_DISABLED` | From GitLab 13.6 | If the variable is present, the job isn't created. |
+| `code_quality` | `CODE_QUALITY_DISABLED` | | If the variable is present, the job isn't created. |
+| `container_scanning` | `CONTAINER_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `dast` | `DAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `dast_environment_deploy` | `DAST_DISABLED_FOR_DEFAULT_BRANCH` or `DAST_DISABLED` | [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17789) | If either variable is present, the job isn't created. |
+| `dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `eslint-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `flawfinder-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `gemnasium-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `gemnasium-maven-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `gemnasium-python-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `gosec-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `kubesec-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `license_management` | `LICENSE_MANAGEMENT_DISABLED` | GitLab 12.7 and earlier | If the variable is present, the job isn't created. Job deprecated [from GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) |
+| `license_scanning` | `LICENSE_MANAGEMENT_DISABLED` | [From GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) | If the variable is present, the job isn't created. |
+| `load_performance` | `LOAD_PERFORMANCE_DISABLED` | From GitLab 13.2 | If the variable is present, the job isn't created. |
+| `nodejs-scan-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `performance` | `PERFORMANCE_DISABLED` | GitLab 13.12 and earlier | Browser performance. If the variable is present, the job isn't created. Replaced by `browser_performance`. |
+| `browser_performance` | `BROWSER_PERFORMANCE_DISABLED` | From GitLab 14.0 | Browser performance. If the variable is present, the job isn't created. Replaces `performance`. |
+| `phpcs-security-audit-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `pmd-apex-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `review` | `REVIEW_DISABLED` | | If the variable is present, the job isn't created. |
+| `review:stop` | `REVIEW_DISABLED` | | Manual job. If the variable is present, the job isn't created. |
+| `sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `sast:container` | `CONTAINER_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
+| `secret_detection` | `SECRET_DETECTION_DISABLED` | From GitLab 13.1 | If the variable is present, the job isn't created. |
+| `secret_detection_default_branch` | `SECRET_DETECTION_DISABLED` | [From GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) | If the variable is present, the job isn't created. |
+| `security-code-scan-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `secrets-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `sobelaw-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `stop_dast_environment` | `DAST_DISABLED_FOR_DEFAULT_BRANCH` or `DAST_DISABLED` | [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17789) | If either variable is present, the job isn't created. |
+| `spotbugs-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
+| `test` | `TEST_DISABLED` | | If the variable is present, the job isn't created. |
+| `staging` | `STAGING_ENABLED` | | The job is created if the variable is present. |
+| `stop_review` | `REVIEW_DISABLED` | | If the variable is present, the job isn't created. |
+
+## Configure application secret variables
+
+Some deployed applications require access to secret variables.
+Auto DevOps detects CI/CD variables starting with `K8S_SECRET_`,
+and makes them available to the deployed application as
+environment variables.
+
+To configure secret variables:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > CI/CD**.
+1. Expand **Variables**.
+1. Create a CI/CD variable with the prefix `K8S_SECRET_`. For example, you
+ can create a variable called `K8S_SECRET_RAILS_MASTER_KEY`.
+1. Run an Auto DevOps pipeline, either by manually creating a new
+ pipeline or by pushing a code change to GitLab.
+
+### Kubernetes secrets
+
+Auto DevOps pipelines use your application secret variables to
+populate a Kubernetes secret. This secret is unique per environment.
+When deploying your application, the secret is loaded as environment
+variables in the container running the application. For example, if
+you create a secret called `K8S_SECRET_RAILS_MASTER_KEY`, your
+Kubernetes secret might look like:
+
+```shell
+$ kubectl get secret production-secret -n minimal-ruby-app-54 -o yaml
+
+apiVersion: v1
+data:
+ RAILS_MASTER_KEY: MTIzNC10ZXN0
+kind: Secret
+metadata:
+ creationTimestamp: 2018-12-20T01:48:26Z
+ name: production-secret
+ namespace: minimal-ruby-app-54
+ resourceVersion: "429422"
+ selfLink: /api/v1/namespaces/minimal-ruby-app-54/secrets/production-secret
+ uid: 57ac2bfd-03f9-11e9-b812-42010a9400e4
+type: Opaque
+```
+
+## Update application secrets
+
+Environment variables are generally immutable in a Kubernetes pod.
+If you update an application secret and then manually
+create a new pipeline, running applications do not receive the
+updated secret.
+
+To update application secrets, either:
+
+- Push a code update to GitLab to force the Kubernetes deployment to recreate pods.
+- Manually delete running pods to cause Kubernetes to create new pods with updated
+ secrets.
+
+Variables with multi-line values are not supported due to
+limitations with the Auto DevOps scripting environment.
+
+## Configure replica variables
+
+Add replica variables when you want to scale your deployments:
+
+1. Add a replica variable as a [project CI/CD variable](../../ci/variables/index.md#add-a-cicd-variable-to-a-project).
+1. To scale your application, redeploy it.
+
+ WARNING:
+ Do not scale your application using Kubernetes directly. Helm might not detect the change,
+ and subsequent deployments with Auto DevOps can undo your changes.
+
+### Custom replica variables
+
+You can create custom replica variables with the format `<TRACK>_<ENV>_REPLICAS`:
+
+- `<TRACK>` is the all-caps value of the `track`
+ [Kubernetes label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
+ set in the Helm Chart app definition. If `track` is not set, omit `<TRACK>` from the custom variable.
+- `<ENV>` is the all-caps environment name of the deploy job set in
+ `.gitlab-ci.yml`.
+
+For example, if the environment is `qa` and the track is
+`foo`, create an environment variable called `FOO_QA_REPLICAS`:
+
+```yaml
+QA testing:
+ stage: deploy
+ environment:
+ name: qa
+ script:
+ - deploy foo
+```
+
+The track `foo` must be defined in the application's Helm chart.
+For example:
+
+```yaml
+replicaCount: 1
+image:
+ repository: gitlab.example.com/group/project
+ tag: stable
+ pullPolicy: Always
+ secrets:
+ - name: gitlab-registry
+application:
+ track: foo
+ tier: web
+service:
+ enabled: true
+ name: web
+ type: ClusterIP
+ url: http://my.host.com/
+ externalPort: 5000
+ internalPort: 5000
+```
+
+## Deploy policy for staging and production environments
+
+Auto DevOps typically uses continuous deployment, and pushes
+automatically to the `production` environment whenever a new pipeline
+runs on the default branch. To deploy to production manually, you can
+use the `STAGING_ENABLED` CI/CD variable.
+
+If you set `STAGING_ENABLED`, GitLab automatically deploys the
+application to a `staging` environment. When you're ready to deploy to
+production, GitLab creates a `production_manual` job.
+
+You can also enable manual deployment in your [project settings](requirements.md#auto-devops-deployment-strategy).
+
+## Deploy policy for canary environments **(PREMIUM)**
+
+You can use a [canary environment](../../user/project/canary_deployments.md) before
+deploying any changes to production.
+
+If you set `CANARY_ENABLED`, GitLab creates two [manual jobs](../../ci/pipelines/index.md#add-manual-interaction-to-your-pipeline):
+
+- `canary` - Deploys the application to the canary environment.
+- `production_manual` - Deploys the application to production.
+
+## Incremental rollout to production **(PREMIUM)**
+
+Use an incremental rollout to continuously deploy your application,
+starting with only a few pods. You can increase the number of pods
+manually.
+
+You can enable manual deployment in your [project settings](requirements.md#auto-devops-deployment-strategy),
+or by setting `INCREMENTAL_ROLLOUT_MODE` to `manual`.
+
+If you set `INCREMENTAL_ROLLOUT_MODE` to `manual`, GitLab creates four
+manual jobs:
+
+1. `rollout 10%`
+1. `rollout 25%`
+1. `rollout 50%`
+1. `rollout 100%`
+
+The percentage is based on the `REPLICAS` CI/CD variable, and defines the number of
+pods used for deployment. For example, if the value is `10` and you run the
+`10%` rollout job, your application is deployed to only one pod.
+
+You can run the rollout jobs in any order. To scale down, rerun a
+lower percentage job.
+
+After you run the `rollout 100%` job, you cannot scale down, and must
+[rollback your deployment](../../ci/environments/index.md#retry-or-roll-back-a-deployment).
+
+### Example incremental rollout configurations
+
+Without `INCREMENTAL_ROLLOUT_MODE` and without `STAGING_ENABLED`:
+
+![Staging and rollout disabled](img/rollout_staging_disabled.png)
+
+Without `INCREMENTAL_ROLLOUT_MODE` and with `STAGING_ENABLED`:
+
+![Staging enabled](img/staging_enabled.png)
+
+With `INCREMENTAL_ROLLOUT_MODE` set to `manual` and without `STAGING_ENABLED`:
+
+![Rollout enabled](img/rollout_enabled.png)
+
+With `INCREMENTAL_ROLLOUT_MODE` set to `manual` and with `STAGING_ENABLED`:
+
+![Rollout and staging enabled](img/rollout_staging_enabled.png)
+
+WARNING:
+This configuration is deprecated, and is scheduled to be removed in the future.
+
+## Timed incremental rollout to production **(PREMIUM)**
+
+Use a timed incremental rollout to continuously deploy your application, starting with
+only a few pods.
+
+You can enable timed incremental deployment in your [project settings](requirements.md#auto-devops-deployment-strategy),
+or by setting the `INCREMENTAL_ROLLOUT_MODE` CI/CD variable to `timed`.
+
+If you set `INCREMENTAL_ROLLOUT_MODE` to `timed`, GitLab creates four jobs:
+
+1. `timed rollout 10%`
+1. `timed rollout 25%`
+1. `timed rollout 50%`
+1. `timed rollout 100%`
+
+There is a five-minute delay between jobs.
diff --git a/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md b/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md
index 8a041b08a4d..78c51572973 100644
--- a/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md
+++ b/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md
@@ -231,7 +231,7 @@ takes you to the pod's logs page.
NOTE:
The example shows only one pod hosting the application at the moment, but you can add
-more pods by defining the [`REPLICAS` CI/CD variable](../customize.md#cicd-variables)
+more pods by defining the [`REPLICAS` CI/CD variable](../cicd_variables.md)
in **Settings > CI/CD > Variables**.
### Work with branches
@@ -300,7 +300,7 @@ and customized to fit your workflow. Here are some helpful resources for further
1. [Auto DevOps](../index.md)
1. [Multiple Kubernetes clusters](../multiple_clusters_auto_devops.md)
-1. [Incremental rollout to production](../customize.md#incremental-rollout-to-production)
-1. [Disable jobs you don't need with CI/CD variables](../customize.md#cicd-variables)
+1. [Incremental rollout to production](../cicd_variables.md#incremental-rollout-to-production)
+1. [Disable jobs you don't need with CI/CD variables](../cicd_variables.md)
1. [Use your own buildpacks to build your application](../customize.md#custom-buildpacks)
1. [Prometheus monitoring](../../../user/project/integrations/prometheus.md)
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index 4dd17badedc..c42f5825b6a 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -105,7 +105,7 @@ You can override this behavior by defining specific variables:
These variables also affect Auto Build and Auto Container Scanning. If you don't want to build and push an image to
`$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG`, consider
-including only `Jobs/Deploy.gitlab-ci.yml`, or [disabling the `build` jobs](#disable-jobs).
+including only `Jobs/Deploy.gitlab-ci.yml`, or [disabling the `build` jobs](cicd_variables.md#job-disabling-variables).
If you use Auto Container Scanning and set a value for `$CI_APPLICATION_REPOSITORY`, then you should
also update `$CS_DEFAULT_BRANCH_IMAGE`. See [Setting the default branch image](../../user/application_security/container_scanning/index.md#setting-the-default-branch-image)
@@ -187,11 +187,11 @@ You can override the default values in the `values.yaml` file in the
- Adding a file named `.gitlab/auto-deploy-values.yaml` to your repository, which is
automatically used, if found.
- Adding a file with a different name or path to the repository, and setting the
- `HELM_UPGRADE_VALUES_FILE` [CI/CD variable](#cicd-variables) with
+ `HELM_UPGRADE_VALUES_FILE` [CI/CD variable](cicd_variables.md) with
the path and name.
Some values cannot be overridden with the options above. Settings like `replicaCount` should instead be overridden with the `REPLICAS`
-[build and deployment](#build-and-deployment) CI/CD variable. Follow [this issue](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/issues/31) for more information.
+[build and deployment](cicd_variables.md#build-and-deployment-variables) CI/CD variable. Follow [this issue](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/issues/31) for more information.
NOTE:
For GitLab 12.5 and earlier, use the `HELM_UPGRADE_EXTRA_ARGS` variable
@@ -308,7 +308,7 @@ You can configure many Auto DevOps jobs to run in an [offline environment](../..
To support applications requiring a database,
[PostgreSQL](https://www.postgresql.org/) is provisioned by default. The credentials to access
the database are preconfigured, but can be customized by setting the associated
-[CI/CD variables](#cicd-variables). You can use these credentials to define a `DATABASE_URL`:
+[CI/CD variables](cicd_variables.md). You can use these credentials to define a `DATABASE_URL`:
```yaml
postgres://user:password@postgres-host:postgres-port/postgres-database
@@ -340,9 +340,9 @@ To set custom values, do one of the following:
- Add a file named `.gitlab/auto-deploy-postgres-values.yaml` to your repository. If found, this
file is used automatically. This file is used by default for PostgreSQL Helm upgrades.
- Add a file with a different name or path to the repository, and set the
- `POSTGRES_HELM_UPGRADE_VALUES_FILE` [environment variable](#database) with the path
+ `POSTGRES_HELM_UPGRADE_VALUES_FILE` [environment variable](cicd_variables.md#database-variables) with the path
and name.
-- Set the `POSTGRES_HELM_UPGRADE_EXTRA_ARGS` [environment variable](#database).
+- Set the `POSTGRES_HELM_UPGRADE_EXTRA_ARGS` [environment variable](cicd_variables.md#database-variables).
### Using external PostgreSQL database providers
@@ -371,342 +371,6 @@ You must define environment-scoped CI/CD variables for `POSTGRES_ENABLED` and
You must ensure that your Kubernetes cluster has network access to wherever
PostgreSQL is hosted.
-## CI/CD variables
-
-The following variables can be used for setting up the Auto DevOps domain,
-providing a custom Helm chart, or scaling your application. PostgreSQL can
-also be customized, and you can use a [custom buildpack](#custom-buildpacks).
-
-### Build and deployment
-
-The following table lists CI/CD variables related to building and deploying
-applications.
-
-| **CI/CD Variable** | **Description** |
-|-----------------------------------------|------------------------------------|
-| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the Ingress hosts. |
-| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the Ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. |
-| `AUTO_BUILD_IMAGE_VERSION` | Customize the image version used for the `build` job. See [list of versions](https://gitlab.com/gitlab-org/cluster-integration/auto-build-image/-/releases). |
-| `AUTO_DEPLOY_IMAGE_VERSION` | Customize the image version used for Kubernetes deployment jobs. See [list of versions](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/releases). |
-| `AUTO_DEVOPS_ATOMIC_RELEASE` | As of GitLab 13.0, Auto DevOps uses [`--atomic`](https://v2.helm.sh/docs/helm/#options-43) for Helm deployments by default. Set this variable to `false` to disable the use of `--atomic` |
-| `AUTO_DEVOPS_BUILD_IMAGE_CNB_ENABLED` | Set to `false` to use Herokuish instead of Cloud Native Buildpacks with Auto Build. [More details](stages.md#auto-build-using-cloud-native-buildpacks). |
-| `AUTO_DEVOPS_BUILD_IMAGE_CNB_BUILDER` | The builder used when building with Cloud Native Buildpacks. The default builder is `heroku/buildpacks:18`. [More details](stages.md#auto-build-using-cloud-native-buildpacks). |
-| `AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` | Extra arguments to be passed to the `docker build` command. Note that using quotes doesn't prevent word splitting. [More details](#passing-arguments-to-docker-build). |
-| `AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES` | A [comma-separated list of CI/CD variable names](#forward-cicd-variables-to-the-build-environment) to be forwarded to the build environment (the buildpack builder or `docker build`). |
-| `AUTO_DEVOPS_BUILD_IMAGE_CNB_PORT` | In GitLab 15.0 and later, port exposed by the generated Docker image. Set to `false` to prevent exposing any ports. Defaults to `5000`. |
-| `AUTO_DEVOPS_CHART` | Helm Chart used to deploy your apps. Defaults to the one [provided by GitLab](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/tree/master/assets/auto-deploy-app). |
-| `AUTO_DEVOPS_CHART_REPOSITORY` | Helm Chart repository used to search for charts. Defaults to `https://charts.gitlab.io`. |
-| `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | Used to set the name of the Helm repository. Defaults to `gitlab`. |
-| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | Used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. |
-| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | Used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. |
-| `AUTO_DEVOPS_CHART_REPOSITORY_PASS_CREDENTIALS` | From GitLab 14.2, set to a non-empty value to enable forwarding of the Helm repository credentials to the chart server when the chart artifacts are on a different host than repository. |
-| `AUTO_DEVOPS_COMMON_NAME` | From GitLab 15.5, set to a valid domain name to customize the common name used for the TLS certificate. Defaults to `le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN`. Set to `false` to not set this alternative host on the Ingress. |
-| `AUTO_DEVOPS_DEPLOY_DEBUG` | From GitLab 13.1, if this variable is present, Helm outputs debug logs. |
-| `AUTO_DEVOPS_ALLOW_TO_FORCE_DEPLOY_V<N>` | From [auto-deploy-image](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image) v1.0.0, if this variable is present, a new major version of chart is forcibly deployed. For more information, see [Ignore warnings and continue deploying](upgrading_auto_deploy_dependencies.md#ignore-warnings-and-continue-deploying). |
-| `BUILDPACK_URL` | Buildpack's full URL. [Must point to a URL supported by Pack or Herokuish](#custom-buildpacks). |
-| `CANARY_ENABLED` | Used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
-| `BUILDPACK_VOLUMES` | Specify one or more [Buildpack volumes to mount](stages.md#mount-volumes-into-the-build-container). Use a pipe `|` as list separator. |
-| `CANARY_PRODUCTION_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. Takes precedence over `CANARY_REPLICAS`. Defaults to 1. |
-| `CANARY_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md). Defaults to 1. |
-| `CI_APPLICATION_REPOSITORY` | The repository of container image being built or deployed, `$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG`. For more details, read [Custom container image](#custom-container-image). |
-| `CI_APPLICATION_TAG` | The tag of the container image being built or deployed, `$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG`. For more details, read [Custom container image](#custom-container-image). |
-| `DAST_AUTO_DEPLOY_IMAGE_VERSION` | Customize the image version used for DAST deployments on the default branch. Should usually be the same as `AUTO_DEPLOY_IMAGE_VERSION`. See [list of versions](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/releases). |
-| `DOCKERFILE_PATH` | From GitLab 13.2, allows overriding the [default Dockerfile path for the build stage](#custom-dockerfile) |
-| `HELM_RELEASE_NAME` | From GitLab 12.1, allows the `helm` release name to be overridden. Can be used to assign unique release names when deploying multiple projects to a single namespace. |
-| `HELM_UPGRADE_VALUES_FILE` | From GitLab 12.6, allows the `helm upgrade` values file to be overridden. Defaults to `.gitlab/auto-deploy-values.yaml`. |
-| `HELM_UPGRADE_EXTRA_ARGS` | Allows extra options in `helm upgrade` commands when deploying the application. Note that using quotes doesn't prevent word splitting. |
-| `INCREMENTAL_ROLLOUT_MODE` | If present, can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. Set to `manual` for manual deployment jobs or `timed` for automatic rollout deployments with a 5 minute delay each one. |
-| `K8S_SECRET_*` | Any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) is made available by Auto DevOps as environment variables to the deployed application. |
-| `KUBE_CONTEXT` | From GitLab 14.5, can be used to select a context to use from `KUBECONFIG`. When `KUBE_CONTEXT` is blank, the default context in `KUBECONFIG` (if any) is used. A context must be selected when used [with the agent for Kubernetes](../../user/clusters/agent/ci_cd_workflow.md). |
-| `KUBE_INGRESS_BASE_DOMAIN` | Can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/gitlab_managed_clusters.md#base-domain) for more information. |
-| `KUBE_NAMESPACE` | The namespace used for deployments. When using certificate-based clusters, [this value should not be overwritten directly](../../user/project/clusters/deploy_to_cluster.md#custom-namespace). |
-| `KUBECONFIG` | The kubeconfig to use for deployments. User-provided values take priority over GitLab-provided values. |
-| `PRODUCTION_REPLICAS` | Number of replicas to deploy in the production environment. Takes precedence over `REPLICAS` and defaults to 1. For zero downtime upgrades, set to 2 or greater. |
-| `REPLICAS` | Number of replicas to deploy. Defaults to 1. Change this variable instead of [modifying](#customize-values-for-helm-chart) `replicaCount`. |
-| `ROLLOUT_RESOURCE_TYPE` | Allows specification of the resource type being deployed when using a custom Helm chart. Default value is `deployment`. |
-| `ROLLOUT_STATUS_DISABLED` | From GitLab 12.0, used to disable rollout status check because it does not support all resource types, for example, `cronjob`. |
-| `STAGING_ENABLED` | Used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
-| `TRACE` | Set to any value to make Helm commands produce verbose output. You can use this setting to help diagnose Auto DevOps deployment problems. |
-
-NOTE:
-After you set up your replica variables using a
-[project CI/CD variable](../../ci/variables/index.md),
-you can scale your application by redeploying it.
-
-WARNING:
-You should *not* scale your application using Kubernetes directly. This can
-cause confusion with Helm not detecting the change, and subsequent deploys with
-Auto DevOps can undo your changes.
-
-### Database
-
-The following table lists CI/CD variables related to the database.
-
-| **CI/CD Variable** | **Description** |
-|-----------------------------------------|------------------------------------|
-| `DB_INITIALIZE` | Used to specify the command to run to initialize the application's PostgreSQL database. Runs inside the application pod. |
-| `DB_MIGRATE` | Used to specify the command to run to migrate the application's PostgreSQL database. Runs inside the application pod. |
-| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled. Defaults to `true`. Set to `false` to disable the automatic deployment of PostgreSQL. |
-| `POSTGRES_USER` | The PostgreSQL user. Defaults to `user`. Set it to use a custom username. |
-| `POSTGRES_PASSWORD` | The PostgreSQL password. Defaults to `testing-password`. Set it to use a custom password. |
-| `POSTGRES_DB` | The PostgreSQL database name. Defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/index.md#predefined-cicd-variables). Set it to use a custom database name. |
-| `POSTGRES_VERSION` | Tag for the [`postgres` Docker image](https://hub.docker.com/_/postgres) to use. Defaults to `9.6.16` for tests and deployments as of GitLab 13.0 (previously `9.6.2`). If `AUTO_DEVOPS_POSTGRES_CHANNEL` is set to `1`, deployments uses the default version `9.6.2`. |
-| `POSTGRES_HELM_UPGRADE_VALUES_FILE` | In GitLab 13.8 and later, and when using [auto-deploy-image v2](upgrading_auto_deploy_dependencies.md), this variable allows the `helm upgrade` values file for PostgreSQL to be overridden. Defaults to `.gitlab/auto-deploy-postgres-values.yaml`. |
-| `POSTGRES_HELM_UPGRADE_EXTRA_ARGS` | In GitLab 13.8 and later, and when using [auto-deploy-image v2](upgrading_auto_deploy_dependencies.md), this variable allows extra PostgreSQL options in `helm upgrade` commands when deploying the application. Note that using quotes doesn't prevent word splitting. |
-| `POSTGRES_CHART_REPOSITORY` | Helm Chart repository used to search for PostgreSQL chart. Defaults to `https://raw.githubusercontent.com/bitnami/charts/eb5f9a9513d987b519f0ecd732e7031241c50328/bitnami`. |
-| `POSTGRES_CHART_VERSION` | Helm Chart version used for PostgreSQL chart. Defaults to `8.2.1`. |
-
-### Disable jobs
-
-The following table lists variables used to disable jobs.
-
-| **Job Name** | **CI/CDVariable** | **GitLab version** | **Description** |
-|----------------------------------------|---------------------------------|-----------------------|-----------------|
-| `.fuzz_base` | `COVFUZZ_DISABLED` | [From GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34984) | [Read more](../../user/application_security/coverage_fuzzing/index.md) about how `.fuzz_base` provide capability for your own jobs. If the variable is present, your jobs aren't created. |
-| `apifuzzer_fuzz` | `API_FUZZING_DISABLED` | [From GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39135) | If the variable is present, the job isn't created. |
-| `build` | `BUILD_DISABLED` | | If the variable is present, the job isn't created. |
-| `build_artifact` | `BUILD_DISABLED` | | If the variable is present, the job isn't created. |
-| `bandit-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `brakeman-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `canary` | `CANARY_ENABLED` | | This manual job is created if the variable is present. |
-| `cluster_image_scanning` | `CLUSTER_IMAGE_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `code_intelligence` | `CODE_INTELLIGENCE_DISABLED` | From GitLab 13.6 | If the variable is present, the job isn't created. |
-| `code_quality` | `CODE_QUALITY_DISABLED` | | If the variable is present, the job isn't created. |
-| `container_scanning` | `CONTAINER_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `dast` | `DAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `dast_environment_deploy` | `DAST_DISABLED_FOR_DEFAULT_BRANCH` or `DAST_DISABLED` | [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17789) | If either variable is present, the job isn't created. |
-| `dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `eslint-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `flawfinder-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `gemnasium-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `gemnasium-maven-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `gemnasium-python-dependency_scanning` | `DEPENDENCY_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `gosec-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `kubesec-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `license_management` | `LICENSE_MANAGEMENT_DISABLED` | GitLab 12.7 and earlier | If the variable is present, the job isn't created. Job deprecated [from GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) |
-| `license_scanning` | `LICENSE_MANAGEMENT_DISABLED` | [From GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) | If the variable is present, the job isn't created. |
-| `load_performance` | `LOAD_PERFORMANCE_DISABLED` | From GitLab 13.2 | If the variable is present, the job isn't created. |
-| `nodejs-scan-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `performance` | `PERFORMANCE_DISABLED` | GitLab 13.12 and earlier | Browser performance. If the variable is present, the job isn't created. Replaced by `browser_performance`. |
-| `browser_performance` | `BROWSER_PERFORMANCE_DISABLED` | From GitLab 14.0 | Browser performance. If the variable is present, the job isn't created. Replaces `performance`. |
-| `phpcs-security-audit-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `pmd-apex-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `review` | `REVIEW_DISABLED` | | If the variable is present, the job isn't created. |
-| `review:stop` | `REVIEW_DISABLED` | | Manual job. If the variable is present, the job isn't created. |
-| `sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `sast:container` | `CONTAINER_SCANNING_DISABLED` | | If the variable is present, the job isn't created. |
-| `secret_detection` | `SECRET_DETECTION_DISABLED` | From GitLab 13.1 | If the variable is present, the job isn't created. |
-| `secret_detection_default_branch` | `SECRET_DETECTION_DISABLED` | [From GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22773) | If the variable is present, the job isn't created. |
-| `security-code-scan-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `secrets-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `sobelaw-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `stop_dast_environment` | `DAST_DISABLED_FOR_DEFAULT_BRANCH` or `DAST_DISABLED` | [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17789) | If either variable is present, the job isn't created. |
-| `spotbugs-sast` | `SAST_DISABLED` | | If the variable is present, the job isn't created. |
-| `test` | `TEST_DISABLED` | | If the variable is present, the job isn't created. |
-| `staging` | `STAGING_ENABLED` | | The job is created if the variable is present. |
-| `stop_review` | `REVIEW_DISABLED` | | If the variable is present, the job isn't created. |
-
-### Application secret variables
-
-Some applications need to define secret variables that are accessible by the deployed
-application. Auto DevOps detects CI/CD variables starting with `K8S_SECRET_`, and makes
-these prefixed variables available to the deployed application as environment variables.
-
-To configure your application variables:
-
-1. On the top bar, select **Main menu > Projects** and find your project.
-1. On the left sidebar, select **Settings > CI/CD**.
-1. Expand **Variables**.
-1. Create a CI/CD variable, ensuring the key is prefixed with
- `K8S_SECRET_`. For example, you can create a variable with key
- `K8S_SECRET_RAILS_MASTER_KEY`.
-
-1. Run an Auto DevOps pipeline, either by manually creating a new
- pipeline or by pushing a code change to GitLab.
-
-Auto DevOps pipelines take your application secret variables to
-populate a Kubernetes secret. This secret is unique per environment.
-When deploying your application, the secret is loaded as environment
-variables in the container running the application. Following the
-example above, you can see the secret below containing the
-`RAILS_MASTER_KEY` variable.
-
-```shell
-$ kubectl get secret production-secret -n minimal-ruby-app-54 -o yaml
-
-apiVersion: v1
-data:
- RAILS_MASTER_KEY: MTIzNC10ZXN0
-kind: Secret
-metadata:
- creationTimestamp: 2018-12-20T01:48:26Z
- name: production-secret
- namespace: minimal-ruby-app-54
- resourceVersion: "429422"
- selfLink: /api/v1/namespaces/minimal-ruby-app-54/secrets/production-secret
- uid: 57ac2bfd-03f9-11e9-b812-42010a9400e4
-type: Opaque
-```
-
-Environment variables are generally considered immutable in a Kubernetes pod.
-If you update an application secret without changing any code, then manually
-create a new pipeline, any running application pods don't receive
-the updated secrets. To update the secrets, either:
-
-- Push a code update to GitLab to force the Kubernetes deployment to recreate pods.
-- Manually delete running pods to cause Kubernetes to create new pods with updated
- secrets.
-
-Variables with multi-line values are not currently supported due to
-limitations with the current Auto DevOps scripting environment.
-
-### Advanced replica variables setup
-
-Apart from the two replica-related variables for production mentioned above,
-you can also use other variables for different environments.
-
-The Kubernetes' label named `track`, GitLab CI/CD environment names, and the
-replicas environment variable are combined into the format `TRACK_ENV_REPLICAS`,
-enabling you to define your own variables for scaling the pod's replicas:
-
-- `TRACK`: The capitalized value of the `track`
- [Kubernetes label](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
- in the Helm Chart app definition. If not set, it isn't taken into account
- to the variable name.
-- `ENV`: The capitalized environment name of the deploy job, set in
- `.gitlab-ci.yml`.
-
-In the example below, the environment's name is `qa`, and it deploys the track
-`foo`, which results in an environment variable named `FOO_QA_REPLICAS`:
-
-```yaml
-QA testing:
- stage: deploy
- environment:
- name: qa
- script:
- - deploy foo
-```
-
-The track `foo` being referenced must also be defined in the application's Helm chart, like:
-
-```yaml
-replicaCount: 1
-image:
- repository: gitlab.example.com/group/project
- tag: stable
- pullPolicy: Always
- secrets:
- - name: gitlab-registry
-application:
- track: foo
- tier: web
-service:
- enabled: true
- name: web
- type: ClusterIP
- url: http://my.host.com/
- externalPort: 5000
- internalPort: 5000
-```
-
-### Deploy policy for staging and production environments
-
-NOTE:
-You can also set this inside your [project's settings](requirements.md#auto-devops-deployment-strategy).
-
-The normal behavior of Auto DevOps is to use continuous deployment, pushing
-automatically to the `production` environment every time a new pipeline is run
-on the default branch. However, there are cases where you might want to use a
-staging environment, and deploy to production manually. For this scenario, the
-`STAGING_ENABLED` CI/CD variable was introduced.
-
-If you define `STAGING_ENABLED` with a non-empty value, then GitLab automatically deploys the application
-to a `staging` environment, and creates a `production_manual` job for
-you when you're ready to manually deploy to production.
-
-### Deploy policy for canary environments **(PREMIUM)**
-
-You can use a [canary environment](../../user/project/canary_deployments.md) before
-deploying any changes to production.
-
-If you define `CANARY_ENABLED` with a non-empty value, then two manual jobs are created:
-
-- `canary` - Deploys the application to the canary environment.
-- `production_manual` - Manually deploys the application to production.
-
-### Incremental rollout to production **(PREMIUM)**
-
-NOTE:
-You can also set this inside your [project's settings](requirements.md#auto-devops-deployment-strategy).
-
-When you're ready to deploy a new version of your app to production, you may want
-to use an incremental rollout to replace just a few pods with the latest code to
-check how the application is behaving before manually increasing the rollout up to 100%.
-
-If `INCREMENTAL_ROLLOUT_MODE` is set to `manual` in your project, then instead
-of the standard `production` job, 4 different
-[manual jobs](../../ci/pipelines/index.md#add-manual-interaction-to-your-pipeline)
-are created:
-
-1. `rollout 10%`
-1. `rollout 25%`
-1. `rollout 50%`
-1. `rollout 100%`
-
-The percentage is based on the `REPLICAS` CI/CD variable, and defines the number of
-pods you want to have for your deployment. If the value is `10`, and you run the
-`10%` rollout job, there is `1` new pod and `9` old ones.
-
-To start a job, select the play icon (**{play}**) next to the job's name. You're not
-required to go from `10%` to `100%`, you can jump to whatever job you want.
-You can also scale down by running a lower percentage job, just before hitting
-`100%`. Once you get to `100%`, you can't scale down, and you'd have to roll
-back by redeploying the old version using the
-[rollback button](../../ci/environments/index.md#retry-or-roll-back-a-deployment) in the
-environment page.
-
-Below, you can see how the pipeline appears if the rollout or staging
-variables are defined.
-
-Without `INCREMENTAL_ROLLOUT_MODE` and without `STAGING_ENABLED`:
-
-![Staging and rollout disabled](img/rollout_staging_disabled.png)
-
-Without `INCREMENTAL_ROLLOUT_MODE` and with `STAGING_ENABLED`:
-
-![Staging enabled](img/staging_enabled.png)
-
-With `INCREMENTAL_ROLLOUT_MODE` set to `manual` and without `STAGING_ENABLED`:
-
-![Rollout enabled](img/rollout_enabled.png)
-
-With `INCREMENTAL_ROLLOUT_MODE` set to `manual` and with `STAGING_ENABLED`
-
-![Rollout and staging enabled](img/rollout_staging_enabled.png)
-
-WARNING:
-This configuration is deprecated, and is scheduled to be removed in the future.
-
-### Timed incremental rollout to production **(PREMIUM)**
-
-NOTE:
-You can also set this inside your [project's settings](requirements.md#auto-devops-deployment-strategy).
-
-This configuration is based on
-[incremental rollout to production](#incremental-rollout-to-production).
-
-Everything behaves the same way, except:
-
-- To enable it, set the `INCREMENTAL_ROLLOUT_MODE` CI/CD variable to `timed`.
-- Instead of the standard `production` job, the following jobs are created with
- a 5 minute delay between each:
-
- 1. `timed rollout 10%`
- 1. `timed rollout 25%`
- 1. `timed rollout 50%`
- 1. `timed rollout 100%`
-
## Auto DevOps banner
The following Auto DevOps banner displays for users with Maintainer or greater
diff --git a/doc/topics/autodevops/multiple_clusters_auto_devops.md b/doc/topics/autodevops/multiple_clusters_auto_devops.md
index af8c54a8edd..cf775a35eb7 100644
--- a/doc/topics/autodevops/multiple_clusters_auto_devops.md
+++ b/doc/topics/autodevops/multiple_clusters_auto_devops.md
@@ -25,7 +25,7 @@ To deploy your environments to different Kubernetes clusters:
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)
+1. [Configure the Auto DevOps CI/CD Pipeline variables](cicd_variables.md#build-and-deployment-variables)
- 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.
@@ -44,8 +44,8 @@ NOTE:
| 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). |
+| 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](cicd_variables.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](cicd_variables.md#incremental-rollout-to-production). |
## Test your configuration
diff --git a/doc/topics/autodevops/prepare_deployment.md b/doc/topics/autodevops/prepare_deployment.md
index 7805f9cd9c6..ebba20ca821 100644
--- a/doc/topics/autodevops/prepare_deployment.md
+++ b/doc/topics/autodevops/prepare_deployment.md
@@ -21,8 +21,8 @@ that works best for your needs:
| Deployment strategy | Setup | Methodology |
|--|--|--|
| **Continuous deployment to production** | Enables [Auto Deploy](stages.md#auto-deploy) with the default branch continuously deployed to production. | Continuous deployment to production.|
-| **Continuous deployment to production using timed incremental rollout** | Sets the [`INCREMENTAL_ROLLOUT_MODE`](customize.md#timed-incremental-rollout-to-production) variable to `timed`. | Continuously deploy to production with a 5 minutes delay between rollouts. |
-| **Automatic deployment to staging, manual deployment to production** | Sets [`STAGING_ENABLED`](customize.md#deploy-policy-for-staging-and-production-environments) to `1` and [`INCREMENTAL_ROLLOUT_MODE`](customize.md#incremental-rollout-to-production) to `manual`. | The default branch is continuously deployed to staging and continuously delivered to production. |
+| **Continuous deployment to production using timed incremental rollout** | Sets the [`INCREMENTAL_ROLLOUT_MODE`](cicd_variables.md#timed-incremental-rollout-to-production) variable to `timed`. | Continuously deploy to production with a 5 minutes delay between rollouts. |
+| **Automatic deployment to staging, manual deployment to production** | Sets [`STAGING_ENABLED`](cicd_variables.md#deploy-policy-for-staging-and-production-environments) to `1` and [`INCREMENTAL_ROLLOUT_MODE`](cicd_variables.md#incremental-rollout-to-production) to `manual`. | The default branch is continuously deployed to staging and continuously delivered to production. |
You can choose the deployment method when enabling Auto DevOps or later:
diff --git a/doc/topics/autodevops/requirements.md b/doc/topics/autodevops/requirements.md
index 7c69e0327c8..f5e06843e60 100644
--- a/doc/topics/autodevops/requirements.md
+++ b/doc/topics/autodevops/requirements.md
@@ -36,8 +36,8 @@ that works best for your needs:
| Deployment strategy | Setup | Methodology |
|--|--|--|
| **Continuous deployment to production** | Enables [Auto Deploy](stages.md#auto-deploy) with the default branch continuously deployed to production. | Continuous deployment to production.|
-| **Continuous deployment to production using timed incremental rollout** | Sets the [`INCREMENTAL_ROLLOUT_MODE`](customize.md#timed-incremental-rollout-to-production) variable to `timed`. | Continuously deploy to production with a 5 minutes delay between rollouts. |
-| **Automatic deployment to staging, manual deployment to production** | Sets [`STAGING_ENABLED`](customize.md#deploy-policy-for-staging-and-production-environments) to `1` and [`INCREMENTAL_ROLLOUT_MODE`](customize.md#incremental-rollout-to-production) to `manual`. | The default branch is continuously deployed to staging and continuously delivered to production. |
+| **Continuous deployment to production using timed incremental rollout** | Sets the [`INCREMENTAL_ROLLOUT_MODE`](cicd_variables.md#timed-incremental-rollout-to-production) variable to `timed`. | Continuously deploy to production with a 5 minutes delay between rollouts. |
+| **Automatic deployment to staging, manual deployment to production** | Sets [`STAGING_ENABLED`](cicd_variables.md#deploy-policy-for-staging-and-production-environments) to `1` and [`INCREMENTAL_ROLLOUT_MODE`](cicd_variables.md#incremental-rollout-to-production) to `manual`. | The default branch is continuously deployed to staging and continuously delivered to production. |
You can choose the deployment method when enabling Auto DevOps or later:
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index 022ee131e40..0ca9c6fa3de 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -385,7 +385,7 @@ default, but the
[Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml)
contains job definitions for these tasks if you want to enable them.
-You can use [CI/CD variables](customize.md#cicd-variables) to automatically
+You can use [CI/CD variables](cicd_variables.md) to automatically
scale your pod replicas, and to apply custom arguments to the Auto DevOps `helm upgrade`
commands. This is an easy way to
[customize the Auto Deploy Helm chart](customize.md#custom-helm-chart).
@@ -620,4 +620,4 @@ for updates.
This stage is enabled by default. You can disable it by adding the
`CODE_INTELLIGENCE_DISABLED` CI/CD variable. Read more about
-[disabling Auto DevOps jobs](../../topics/autodevops/customize.md#disable-jobs).
+[disabling Auto DevOps jobs](../../topics/autodevops/cicd_variables.md#job-disabling-variables).
diff --git a/doc/topics/autodevops/troubleshooting.md b/doc/topics/autodevops/troubleshooting.md
index ae3cc42223f..bd8b40dc500 100644
--- a/doc/topics/autodevops/troubleshooting.md
+++ b/doc/topics/autodevops/troubleshooting.md
@@ -13,7 +13,7 @@ Auto DevOps, and any available workarounds.
Set the CI/CD variable `TRACE` to any value to make Helm commands produce verbose output. You can use this output to diagnose Auto DevOps deployment problems.
-You can resolve some problems with Auto DevOps deployment by changing advanced Auto DevOps configuration variables. Read more about [customizing Auto DevOps CI/CD variables](customize.md#cicd-variables).
+You can resolve some problems with Auto DevOps deployment by changing advanced Auto DevOps configuration variables. Read more about [customizing Auto DevOps CI/CD variables](cicd_variables.md).
## Unable to select a buildpack
diff --git a/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md b/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
index 602bdec6140..4fafc89cac1 100644
--- a/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
+++ b/doc/topics/autodevops/upgrading_auto_deploy_dependencies.md
@@ -128,7 +128,7 @@ with the [v1 auto-deploy-image](#use-a-specific-version-of-auto-deploy-dependenc
> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/merge_requests/109) in GitLab 13.4.
-Auto Deploy supports advanced deployment strategies such as [canary deployments](customize.md#deploy-policy-for-canary-environments)
+Auto Deploy supports advanced deployment strategies such as [canary deployments](cicd_variables.md#deploy-policy-for-canary-environments)
and [incremental rollouts](../../ci/environments/incremental_rollouts.md).
Previously, `auto-deploy-image` created one service to balance the traffic between
@@ -171,7 +171,7 @@ Alternatively, you can use the [v13.12 Auto DevOps templates archive](https://gi
### Ignore warnings and continue deploying
If you are certain that the new chart version is safe to be deployed, you can add
-the `AUTO_DEVOPS_FORCE_DEPLOY_V<major-version-number>` [CI/CD variable](customize.md#build-and-deployment)
+the `AUTO_DEVOPS_FORCE_DEPLOY_V<major-version-number>` [CI/CD variable](cicd_variables.md#build-and-deployment-variables)
to force the deployment to continue.
For example, if you want to deploy the `v2.0.0` chart on a deployment that previously
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index b5049dfb66d..4a82b72d752 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -374,12 +374,12 @@ a^2+b^2=c^2
#### LaTeX-compatible fencing
-> Introduced in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `markdown_dollar_math`. Disabled by default.
+> Introduced in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `markdown_dollar_math`. Disabled by default. Enabled on GitLab.com.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#latex-compatible-fencing).
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available,
+On self-managed GitLab, by default this feature is not available. To make it available per group,
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `markdown_dollar_math`.
On GitLab.com, this feature is available.
The feature is not ready for production use.
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index f6ed9654882..a4ec8be5b70 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -283,6 +283,32 @@ characters are removed.
A `pip install` request for `my.package` looks for packages that match any of
the three characters, such as `my-package`, `my_package`, and `my....package`.
+## Using `requirements.txt`
+
+If you want pip to access your private registry, add the `--extra-index-url` parameter along with the URL for your registry to your `requirements.txt` file.
+
+```plaintext
+--extra-index-url https://gitlab.example.com/api/v4/projects/<project_id>/packages/pypi/simple
+package-name==1.0.0
+```
+
+If this is a private registry, you can authenticate in a couple of ways. For example:
+
+- Using your `requirements.txt` file:
+
+```plaintext
+--extra-index-url https://__token__:<your_personal_token>@gitlab.example.com/api/v4/projects/<project_id>/packages/pypi/simple
+package-name==1.0.0
+```
+
+- Using a `~/.netrc` file:
+
+```plaintext
+machine gitlab.example.com
+login __token__
+password <your_personal_token>
+```
+
## Troubleshooting
To improve performance, the pip command caches files related to a package. Note that pip doesn't remove data by
diff --git a/doc/user/project/badges.md b/doc/user/project/badges.md
index 5d1d10fc37d..dc650bd9482 100644
--- a/doc/user/project/badges.md
+++ b/doc/user/project/badges.md
@@ -89,6 +89,8 @@ which are evaluated when displaying the badge. The following placeholders
are available:
- `%{project_path}`: Path of a project including the parent groups
+- `%{project_title}`: Title of a project
+- `%{project_name}`: Name of a project
- `%{project_id}`: Database ID associated with a project
- `%{default_branch}`: Default branch name configured for a project's repository
- `%{commit_sha}`: ID of the most recent commit to the default branch of a
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
index 3e6a9acc304..95f38c7e354 100644
--- a/doc/user/project/canary_deployments.md
+++ b/doc/user/project/canary_deployments.md
@@ -68,7 +68,7 @@ Here's an example setup flow from scratch:
If it isn't, follow the documentation to specify the image version.
1. [Run a new Auto DevOps pipeline](../../ci/pipelines/index.md#run-a-pipeline-manually)
and make sure that the `production` job succeeds and creates a production environment.
-1. Configure a [`canary` deployment job for Auto DevOps pipelines](../../topics/autodevops/customize.md#deploy-policy-for-canary-environments).
+1. Configure a [`canary` deployment job for Auto DevOps pipelines](../../topics/autodevops/cicd_variables.md#deploy-policy-for-canary-environments).
1. [Run a new Auto DevOps pipeline](../../ci/pipelines/index.md#run-a-pipeline-manually)
and make sure that the `canary` job succeeds and creates a canary deployment with Canary Ingress.
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index b3d381c3148..52288af101a 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -195,7 +195,7 @@ If a default Storage Class doesn't already exist and is desired, follow Amazon's
to create one.
Alternatively, disable PostgreSQL by setting the project variable
-[`POSTGRES_ENABLED`](../../../topics/autodevops/customize.md#cicd-variables) to `false`.
+[`POSTGRES_ENABLED`](../../../topics/autodevops/cicd_variables.md#cicd-variables) to `false`.
## Deploy the app to EKS
diff --git a/doc/user/project/merge_requests/reviews/img/suggestion_code_block_editor_v12_8.png b/doc/user/project/merge_requests/reviews/img/suggestion_code_block_editor_v12_8.png
deleted file mode 100644
index 927b4f812a5..00000000000
--- a/doc/user/project/merge_requests/reviews/img/suggestion_code_block_editor_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/reviews/suggestions.md b/doc/user/project/merge_requests/reviews/suggestions.md
index 832f78d18a1..668dece9fda 100644
--- a/doc/user/project/merge_requests/reviews/suggestions.md
+++ b/doc/user/project/merge_requests/reviews/suggestions.md
@@ -74,7 +74,13 @@ To add a suggestion that includes a
[fenced code block](../../../markdown.md#code-spans-and-blocks), wrap your suggestion
in four backticks instead of three:
-![A comment editor with a suggestion with a fenced code block](img/suggestion_code_block_editor_v12_8.png)
+~~~markdown
+````suggestion:-0+2
+```shell
+git config --global receive.advertisepushoptions true
+```
+````
+~~~
![Output of a comment with a suggestion with a fenced code block](img/suggestion_code_block_output_v12_8.png)
diff --git a/lefthook.yml b/lefthook.yml
index 05703b7a8d9..359c633e0f1 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -86,6 +86,9 @@ pre-push:
files: git diff --name-only --diff-filter=d $(git merge-base origin/master HEAD)..HEAD
glob: 'data/removals/*.yml'
run: echo "Changes to removals files detected. Checking removals..\n"; bundle exec rake gitlab:docs:check_removals
+ scripts:
+ "merge_conflicts":
+ runner: bash
pre-commit:
parallel: true
commands:
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 440f4af0296..b0b79b994c1 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -56,11 +56,13 @@ module Gitlab
strong_memoize_attr :included?, :inclusion
def errors
- # We check rules errors before checking "included?" because rules affects its inclusion status.
- return rules_errors if rules_errors
- return unless included?
+ logger.instrument(:pipeline_seed_build_errors) do
+ # We check rules errors before checking "included?" because rules affects its inclusion status.
+ next rules_errors if rules_errors
+ next unless included?
- [needs_errors, variable_expansion_errors].compact.flatten
+ [needs_errors, variable_expansion_errors].compact.flatten
+ end
end
strong_memoize_attr :errors
@@ -88,17 +90,19 @@ module Gitlab
end
def to_resource
- if reuse_build_in_seed_context?
- # The `options` attribute need to be entirely reassigned because they may
- # be overridden by evaluated_attributes.
- # We also don't want to reassign all the `initial_attributes` since those
- # can affect performance. We only want to assign what's changed.
- assignable_attributes = initial_attributes.slice(:options)
- .deep_merge(evaluated_attributes)
- processable.assign_attributes(assignable_attributes)
- processable
- else
- legacy_initialize_processable
+ logger.instrument(:pipeline_seed_build_to_resource) do
+ if reuse_build_in_seed_context?
+ # The `options` attribute need to be entirely reassigned because they may
+ # be overridden by evaluated_attributes.
+ # We also don't want to reassign all the `initial_attributes` since those
+ # can affect performance. We only want to assign what's changed.
+ assignable_attributes = initial_attributes.slice(:options)
+ .deep_merge(evaluated_attributes)
+ processable.assign_attributes(assignable_attributes)
+ processable
+ else
+ legacy_initialize_processable
+ end
end
end
strong_memoize_attr :to_resource
diff --git a/lib/gitlab/ci/pipeline/seed/pipeline.rb b/lib/gitlab/ci/pipeline/seed/pipeline.rb
index 9e609debeed..57ad2546f1c 100644
--- a/lib/gitlab/ci/pipeline/seed/pipeline.rb
+++ b/lib/gitlab/ci/pipeline/seed/pipeline.rb
@@ -38,8 +38,10 @@ module Gitlab
private
+ delegate :logger, to: :@context
+
def stage_seeds
- strong_memoize(:stage_seeds) do
+ logger.instrument(:pipeline_seed_stage_seeds) do
seeds = @stages_attributes.inject([]) do |previous_stages, attributes|
seed = Gitlab::Ci::Pipeline::Seed::Stage.new(@context, attributes, previous_stages)
previous_stages + [seed]
@@ -48,6 +50,7 @@ module Gitlab
seeds.select(&:included?)
end
end
+ strong_memoize_attr :stage_seeds
end
end
end
diff --git a/lib/gitlab/github_gists_import/importer/gists_importer.rb b/lib/gitlab/github_gists_import/importer/gists_importer.rb
new file mode 100644
index 00000000000..08744dbaf5f
--- /dev/null
+++ b/lib/gitlab/github_gists_import/importer/gists_importer.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubGistsImport
+ module Importer
+ class GistsImporter
+ attr_reader :user, :client, :already_imported_cache_key
+
+ ALREADY_IMPORTED_CACHE_KEY = 'github-gists-importer/already-imported/%{user}'
+ RESULT_CONTEXT = Struct.new(:success?, :error, :waiter, :next_attempt_in, keyword_init: true)
+
+ def initialize(user, token)
+ @user = user
+ @client = Gitlab::GithubImport::Client.new(token, parallel: true)
+ @already_imported_cache_key = format(ALREADY_IMPORTED_CACHE_KEY, user: user.id)
+ end
+
+ def execute
+ waiter = spread_parallel_import
+
+ expire_already_imported_cache!
+
+ RESULT_CONTEXT.new(success?: true, waiter: waiter)
+ rescue Gitlab::GithubImport::RateLimitError => e
+ RESULT_CONTEXT.new(success?: false, error: e, next_attempt_in: client.rate_limit_resets_in)
+ rescue StandardError => e
+ RESULT_CONTEXT.new(success?: false, error: e)
+ end
+
+ private
+
+ def spread_parallel_import
+ waiter = JobWaiter.new
+ worker_arguments = fetch_gists_to_import.map { |gist_hash| [user.id, gist_hash, waiter.key] }
+ waiter.jobs_remaining = worker_arguments.size
+
+ schedule_bulk_perform(worker_arguments)
+ waiter
+ end
+
+ def fetch_gists_to_import
+ page_counter = Gitlab::GithubImport::PageCounter.new(user, :gists, 'github-gists-importer')
+ collection = []
+
+ client.each_page(:gists, nil, page: page_counter.current) do |page|
+ next unless page_counter.set(page.number)
+
+ collection += gists_from(page)
+ end
+
+ page_counter.expire!
+
+ collection
+ end
+
+ def gists_from(page)
+ page.objects.each.with_object([]) do |gist, page_collection|
+ gist = gist.to_h
+ next if already_imported?(gist)
+
+ page_collection << ::Gitlab::GithubGistsImport::Representation::Gist.from_api_response(gist).to_hash
+
+ mark_as_imported(gist)
+ end
+ end
+
+ def schedule_bulk_perform(worker_arguments)
+ # rubocop:disable Scalability/BulkPerformWithContext
+ Gitlab::ApplicationContext.with_context(user: user) do
+ Gitlab::GithubGistsImport::ImportGistWorker.bulk_perform_in(
+ 1.second,
+ worker_arguments,
+ batch_size: 1000,
+ batch_delay: 1.minute
+ )
+ end
+ # rubocop:enable Scalability/BulkPerformWithContext
+ end
+
+ def already_imported?(gist)
+ Gitlab::Cache::Import::Caching.set_includes?(already_imported_cache_key, gist[:id])
+ end
+
+ def mark_as_imported(gist)
+ Gitlab::Cache::Import::Caching.set_add(already_imported_cache_key, gist[:id])
+ end
+
+ def expire_already_imported_cache!
+ Gitlab::Cache::Import::Caching
+ .expire(already_imported_cache_key, Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/page_counter.rb b/lib/gitlab/github_import/page_counter.rb
index 3face4c794b..c238ccb8932 100644
--- a/lib/gitlab/github_import/page_counter.rb
+++ b/lib/gitlab/github_import/page_counter.rb
@@ -9,10 +9,10 @@ module Gitlab
attr_reader :cache_key
# The base cache key to use for storing the last page number.
- CACHE_KEY = 'github-importer/page-counter/%{project}/%{collection}'
+ CACHE_KEY = '%{import_type}/page-counter/%{object}/%{collection}'
- def initialize(project, collection)
- @cache_key = CACHE_KEY % { project: project.id, collection: collection }
+ def initialize(object, collection, import_type = 'github-importer')
+ @cache_key = CACHE_KEY % { import_type: import_type, object: object.id, collection: collection }
end
# Sets the page number to the given value.
diff --git a/lib/gitlab/graphql/limit/field_call_count.rb b/lib/gitlab/graphql/limit/field_call_count.rb
index 4165970a2a6..3a02e8abbb5 100644
--- a/lib/gitlab/graphql/limit/field_call_count.rb
+++ b/lib/gitlab/graphql/limit/field_call_count.rb
@@ -14,9 +14,18 @@ module Gitlab
private
def increment_call_count(context)
+ query_id = fetch_query_id(context)
+
context[:call_count] ||= {}
- context[:call_count][field] ||= 0
- context[:call_count][field] += 1
+ context[:call_count][query_id] ||= {}
+ context[:call_count][query_id][field] ||= 0
+ context[:call_count][query_id][field] += 1
+ end
+
+ def fetch_query_id(context)
+ context.query.operation_fingerprint
+ rescue TypeError
+ ''
end
def limit
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index 40f5359adcf..9bc0001be81 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -80,7 +80,8 @@ module Gitlab
ProjectTemplate.new('tencent_serverless_framework', 'Tencent Serverless Framework/NextjsSSR', _('A project boilerplate for Tencent Serverless Framework that uses Next.js SSR'), 'https://gitlab.com/gitlab-org/project-templates/nextjsssr_demo', 'illustrations/logos/tencent_serverless_framework.svg'),
ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'),
ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management'),
- ProjectTemplate.new('kotlin_native_linux', 'Kotlin Native Linux', _('A basic template for developing Linux programs using Kotlin Native'), 'https://gitlab.com/gitlab-org/project-templates/kotlin-native-linux')
+ ProjectTemplate.new('kotlin_native_linux', 'Kotlin Native Linux', _('A basic template for developing Linux programs using Kotlin Native'), 'https://gitlab.com/gitlab-org/project-templates/kotlin-native-linux'),
+ ProjectTemplate.new('typo3_distribution', 'TYPO3 Distribution', _('A template for starting a new TYPO3 project'), 'https://gitlab.com/ochorocho/typo3-distribution', 'illustrations/logos/typo3.svg')
].freeze
end
# rubocop:enable Metrics/AbcSize
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cc0fadac6f5..88e1db1c847 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1768,6 +1768,9 @@ msgstr ""
msgid "A sign-in to your account has been made from the following IP address: %{ip}"
msgstr ""
+msgid "A template for starting a new TYPO3 project"
+msgstr ""
+
msgid "A title is required"
msgstr ""
@@ -7224,9 +7227,93 @@ msgstr ""
msgid "Broadcast Messages"
msgstr ""
+msgid "BroadcastMessages|Add broadcast message"
+msgstr ""
+
+msgid "BroadcastMessages|Allow users to dismiss the broadcast message"
+msgstr ""
+
+msgid "BroadcastMessages|Banner"
+msgstr ""
+
+msgid "BroadcastMessages|Blue"
+msgstr ""
+
+msgid "BroadcastMessages|Dark"
+msgstr ""
+
+msgid "BroadcastMessages|Dismissable"
+msgstr ""
+
+msgid "BroadcastMessages|Ends at"
+msgstr ""
+
+msgid "BroadcastMessages|Green"
+msgstr ""
+
+msgid "BroadcastMessages|Indigo"
+msgstr ""
+
+msgid "BroadcastMessages|Light"
+msgstr ""
+
+msgid "BroadcastMessages|Light Blue"
+msgstr ""
+
+msgid "BroadcastMessages|Light Green"
+msgstr ""
+
+msgid "BroadcastMessages|Light Indigo"
+msgstr ""
+
+msgid "BroadcastMessages|Light Red"
+msgstr ""
+
+msgid "BroadcastMessages|Message"
+msgstr ""
+
+msgid "BroadcastMessages|Notification"
+msgstr ""
+
+msgid "BroadcastMessages|Paths can contain wildcards, like */welcome"
+msgstr ""
+
+msgid "BroadcastMessages|Red"
+msgstr ""
+
+msgid "BroadcastMessages|Starts at"
+msgstr ""
+
+msgid "BroadcastMessages|Target Path"
+msgstr ""
+
+msgid "BroadcastMessages|Target roles"
+msgstr ""
+
+msgid "BroadcastMessages|The broadcast message displays only to users in projects and groups who have these roles."
+msgstr ""
+
+msgid "BroadcastMessages|Theme"
+msgstr ""
+
+msgid "BroadcastMessages|There was an error adding broadcast message."
+msgstr ""
+
+msgid "BroadcastMessages|There was an error updating broadcast message."
+msgstr ""
+
msgid "BroadcastMessages|There was an issue deleting this message, please try again later."
msgstr ""
+msgid "BroadcastMessages|Type"
+msgstr ""
+
+msgid "BroadcastMessages|Update broadcast message"
+msgstr ""
+
+msgid "BroadcastMessages|Your message here"
+msgstr ""
+
msgid "Browse Directory"
msgstr ""
@@ -32884,6 +32971,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|TYPO3 Distribution"
+msgstr ""
+
msgid "ProjectTemplates|Tencent Serverless Framework/NextjsSSR"
msgstr ""
@@ -37247,9 +37337,6 @@ msgstr ""
msgid "SecurityReports|Although it's rare to have no vulnerabilities, it can happen. Check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityReports|At GitLab, we're all about iteration and feedback. That's why we are reaching out to customers like you to help guide what we work on this year for Vulnerability Management. We have a lot of exciting ideas and ask that you assist us by taking a short survey %{boldStart}no longer than 10 minutes%{boldEnd} to evaluate a few of our potential features."
-msgstr ""
-
msgid "SecurityReports|Change status"
msgstr ""
@@ -37469,9 +37556,6 @@ msgstr ""
msgid "SecurityReports|Submit vulnerability"
msgstr ""
-msgid "SecurityReports|Take survey"
-msgstr ""
-
msgid "SecurityReports|The Vulnerability Report shows results of successful scans on your project's default branch, manually added vulnerability records, and vulnerabilities found from scanning operational environments. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
@@ -37529,9 +37613,6 @@ msgstr ""
msgid "SecurityReports|Upgrade to manage vulnerabilities"
msgstr ""
-msgid "SecurityReports|Vulnerability Management feature survey"
-msgstr ""
-
msgid "SecurityReports|Vulnerability Report"
msgstr ""
@@ -37550,9 +37631,6 @@ msgstr ""
msgid "SecurityReports|You must sign in as an authorized user to see this report"
msgstr ""
-msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
-msgstr ""
-
msgid "SecurityReports|scanned resources"
msgstr ""
@@ -43561,24 +43639,12 @@ msgstr ""
msgid "Trials|Day %{daysUsed}/%{duration}"
msgstr ""
-msgid "Trials|Go back to GitLab"
-msgstr ""
-
-msgid "Trials|Skip Trial"
-msgstr ""
-
msgid "Trials|Upgrade %{groupName} to %{planName}"
msgstr ""
-msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start an Ultimate trial'"
-msgstr ""
-
msgid "Trials|You can apply your trial to a new group or an existing group."
msgstr ""
-msgid "Trials|You won't get a free trial right now but you can always resume this process by selecting your avatar and choosing 'Start an Ultimate trial'"
-msgstr ""
-
msgid "Trials|You've got %{daysRemaining} day remaining on GitLab %{planName}!"
msgid_plural "Trials|You've got %{daysRemaining} days remaining on GitLab %{planName}!"
msgstr[0] ""
diff --git a/package.json b/package.json
index fbaec978776..4b1cbb96fe2 100644
--- a/package.json
+++ b/package.json
@@ -56,7 +56,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.0.0",
"@gitlab/svgs": "3.13.0",
- "@gitlab/ui": "52.0.0",
+ "@gitlab/ui": "52.3.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20221212205235",
"@rails/actioncable": "6.1.4-7",
diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
index 786c0e83853..04aa1b779ce 100644
--- a/qa/lib/gitlab/page/group/settings/usage_quotas.rb
+++ b/qa/lib/gitlab/page/group/settings/usage_quotas.rb
@@ -35,7 +35,7 @@ module Gitlab
div :purchased_usage_total_free # Different UI for free namespace
span :purchased_usage_total
div :storage_purchase_successful_alert, text: /You have successfully purchased a storage/
- h2 :storage_available_alert, text: /purchased storage is available/
+ div :additional_storage_alert, text: /purchase additional storage/
def plan_ci_limits
plan_ci_minutes[/(\d+){2}/]
@@ -48,8 +48,8 @@ module Gitlab
# Waits and Checks if storage available alert presents on the page
#
# @return [Boolean] True if the alert presents, false if not after 5 second wait
- def purchased_storage_available?
- storage_available_alert_element.wait_until(timeout: 5, &:present?)
+ def additional_storage_available?
+ additional_storage_alert_element.wait_until(timeout: 5, &:present?)
rescue Watir::Wait::TimeoutError
false
end
@@ -67,7 +67,7 @@ module Gitlab
#
# @return [Float] Total purchased storage value in GiB
def total_purchased_storage(free_name_space = true)
- storage_available_alert_element.wait_until(&:present?)
+ additional_storage_alert_element.wait_until(&:present?)
if free_name_space
purchased_usage_total_free.split('/').last.match(/\d+\.\d+/)[0].to_f
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 3a5abd0a7a6..30fd04b1c3e 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -90,6 +90,22 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
+ it 'shows a running status badge that links to jobs tab' do
+ runner = create(:ci_runner, :project, projects: [project])
+ job = create(:ci_build, :running, runner: runner)
+
+ visit admin_runners_path
+
+ within_runner_row(runner.id) do
+ click_on(s_('Runners|Running'))
+ end
+
+ expect(current_url).to match(admin_runner_path(runner))
+
+ expect(find("[data-testid='td-status']")).to have_content "running"
+ expect(find("[data-testid='td-job']")).to have_content "##{job.id}"
+ end
+
describe 'search' do
before_all do
create(:ci_runner, :instance, description: 'runner-foo')
diff --git a/spec/features/alert_management/alert_details_spec.rb b/spec/features/alert_management/alert_details_spec.rb
index abb97d3be4d..45fa4d810aa 100644
--- a/spec/features/alert_management/alert_details_spec.rb
+++ b/spec/features/alert_management/alert_details_spec.rb
@@ -30,6 +30,8 @@ RSpec.describe 'Alert details', :js, feature_category: :incident_management do
alert_tabs = find('[data-testid="alertDetailsTabs"]')
expect(alert_tabs).to have_content('Alert details')
+ expect(alert_tabs).to have_content('Metrics')
+ expect(alert_tabs).to have_content('Activity feed')
end
end
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 982c3a07334..bbc14368d82 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js, featu
wait_for_requests
- expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk')
+ expect(page).to have_selector('.ref-selector ', text: '1-cherry-coloured-funk')
expect(page).to have_current_path project_tree_path(project, '1-cherry-coloured-funk'), ignore_query: true
end
end
@@ -109,7 +109,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js, featu
wait_for_requests
- expect(page).to have_selector('.dropdown-toggle-text ', text: branch_name)
+ expect(page).to have_selector('.ref-selector', text: branch_name)
expect(page).to have_current_path project_tree_path(project, branch_name), ignore_query: true
end
end
diff --git a/spec/features/jira_connect/branches_spec.rb b/spec/features/jira_connect/branches_spec.rb
index bee31949dd4..585aa85b16d 100644
--- a/spec/features/jira_connect/branches_spec.rb
+++ b/spec/features/jira_connect/branches_spec.rb
@@ -49,16 +49,11 @@ RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integ
expect(page).to have_button('Create branch', disabled: false)
click_on 'master'
+ fill_in 'Search', with: source_branch
+ expect(page).not_to have_text(source_branch)
- within_dropdown do
- fill_in 'Search', with: source_branch
-
- expect(page).not_to have_text(source_branch)
-
- fill_in 'Search', with: 'master'
-
- expect(page).to have_text('master')
- end
+ fill_in 'Search', with: 'master'
+ expect(page).to have_text('master')
# Switch to project2
@@ -70,10 +65,9 @@ RSpec.describe 'Create GitLab branches from Jira', :js, feature_category: :integ
end
click_on 'master'
-
- within_dropdown do
- fill_in 'Search', with: source_branch
- click_on source_branch
+ fill_in 'Search', with: source_branch
+ within '[role="listbox"]' do
+ find('li', text: source_branch).click
end
fill_in 'Branch name', with: new_branch
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 912e8e6e0bb..125f7209ab4 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -271,6 +271,8 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
context "when browsing a specific ref", :js do
let(:ref) { project_tree_path(project, "6d39438") }
+ ref_selector = '.ref-selector'
+
before do
visit(ref)
end
@@ -281,24 +283,34 @@ RSpec.describe "User browses files", :js, feature_category: :projects do
end
it "shows files from a repository with apostroph in its name" do
- first(".js-project-refs-dropdown").click
+ ref_name = 'test'
+
+ find(ref_selector).click
+ wait_for_requests
- page.within(".project-refs-form") do
- click_link("'test'")
+ page.within(ref_selector) do
+ fill_in 'Search by Git revision', with: ref_name
+ wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
- expect(page).to have_selector(".dropdown-toggle-text", text: "'test'")
+ expect(find(ref_selector)).to have_text(ref_name)
- visit(project_tree_path(project, "'test'"))
+ visit(project_tree_path(project, ref_name))
expect(page).not_to have_selector(".tree-commit .animation-container")
end
it "shows the code with a leading dot in the directory" do
- first(".js-project-refs-dropdown").click
+ ref_name = 'fix'
+
+ find(ref_selector).click
+ wait_for_requests
- page.within(".project-refs-form") do
- click_link("fix")
+ page.within(ref_selector) do
+ fill_in 'Search by Git revision', with: ref_name
+ wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
visit(project_tree_path(project, "fix/.testdir"))
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index f65c32a7a36..21932cae58b 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -157,17 +157,22 @@ RSpec.describe 'Projects tree', :js, feature_category: :web_ide do
end
end
- context 'ref switcher' do
+ context 'ref switcher', :js do
it 'switches ref to branch' do
+ ref_selector = '.ref-selector'
ref_name = 'feature'
visit project_tree_path(project, 'master')
- first('.js-project-refs-dropdown').click
- page.within '.project-refs-form' do
- click_link ref_name
+ find(ref_selector).click
+ wait_for_requests
+
+ page.within(ref_selector) do
+ fill_in 'Search by Git revision', with: ref_name
+ wait_for_requests
+ find('li', text: ref_name, match: :prefer_exact).click
end
- expect(page).to have_selector '.dropdown-menu-toggle', text: ref_name
+ expect(find(ref_selector)).to have_text(ref_name)
end
end
end
diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb
index 1055b621ddd..de52f0b517e 100644
--- a/spec/features/users/email_verification_on_login_spec.rb
+++ b/spec/features/users/email_verification_on_login_spec.rb
@@ -223,6 +223,14 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
it_behaves_like 'email verification required'
it_behaves_like 'no email verification required when 2fa enabled or ff disabled'
+
+ context 'when the check_ip_address_for_email_verification feature flag is disabled' do
+ before do
+ stub_feature_flags(check_ip_address_for_email_verification: false)
+ end
+
+ it_behaves_like 'no email verification required'
+ end
end
describe 'when a previous authentication event exists for the same ip address' do
diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb
index 0a326699875..73a65d0c5af 100644
--- a/spec/finders/ci/auth_job_finder_spec.rb
+++ b/spec/finders/ci/auth_job_finder_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Ci::AuthJobFinder do
+RSpec.describe Ci::AuthJobFinder, feature_category: :continuous_integration do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:job, reload: true) { create(:ci_build, status: :running, user: user) }
@@ -68,7 +68,7 @@ RSpec.describe Ci::AuthJobFinder do
it 'sets ci_job_token_scope on the job user', :aggregate_failures do
expect(subject).to eq(job)
expect(subject.user).to be_from_ci_job_token
- expect(subject.user.ci_job_token_scope.source_project).to eq(job.project)
+ expect(subject.user.ci_job_token_scope.current_project).to eq(job.project)
end
end
end
diff --git a/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js b/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js
new file mode 100644
index 00000000000..291c3aed1cf
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/datetime_picker_spec.js
@@ -0,0 +1,46 @@
+import { mount } from '@vue/test-utils';
+import { GlDatepicker } from '@gitlab/ui';
+import DatetimePicker from '~/admin/broadcast_messages/components/datetime_picker.vue';
+
+describe('DatetimePicker', () => {
+ let wrapper;
+
+ const toDate = (day, time) => new Date(`${day}T${time}:00.000Z`);
+ const findDatepicker = () => wrapper.findComponent(GlDatepicker);
+ const findTimepicker = () => wrapper.findComponent('[data-testid="time-picker"]');
+
+ const testDay = '2022-03-22';
+ const testTime = '01:23';
+
+ function createComponent() {
+ wrapper = mount(DatetimePicker, {
+ propsData: {
+ value: toDate(testDay, testTime),
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the Date in the datepicker and timepicker inputs', () => {
+ expect(findDatepicker().props().value).toEqual(toDate(testDay, testTime));
+ expect(findTimepicker().element.value).toEqual(testTime);
+ });
+
+ it('emits Date with the new day/old time when the date picker changes', () => {
+ const newDay = '1992-06-30';
+ const newTime = '08:00';
+
+ findDatepicker().vm.$emit('input', toDate(newDay, newTime));
+ expect(wrapper.emitted().input).toEqual([[toDate(newDay, testTime)]]);
+ });
+
+ it('emits Date with the old day/new time when the time picker changes', () => {
+ const newTime = '08:00';
+
+ findTimepicker().vm.$emit('input', newTime);
+ expect(wrapper.emitted().input).toEqual([[toDate(testDay, newTime)]]);
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
new file mode 100644
index 00000000000..88ea79f38b3
--- /dev/null
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -0,0 +1,201 @@
+import { mount } from '@vue/test-utils';
+import { GlBroadcastMessage, GlForm } from '@gitlab/ui';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { createAlert } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
+import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
+import {
+ BROADCAST_MESSAGES_PATH,
+ TYPE_BANNER,
+ TYPE_NOTIFICATION,
+ THEMES,
+} from '~/admin/broadcast_messages/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('MessageForm', () => {
+ let wrapper;
+ let axiosMock;
+
+ const defaultProps = {
+ message: 'zzzzzzz',
+ broadcastType: TYPE_BANNER,
+ theme: THEMES[0].value,
+ dismissable: false,
+ targetPath: '',
+ targetAccessLevels: [],
+ startsAt: new Date(),
+ endsAt: new Date(),
+ };
+
+ const findPreview = () => extendedWrapper(wrapper.findComponent(GlBroadcastMessage));
+ const findThemeSelect = () => wrapper.findComponent('[data-testid=theme-select]');
+ const findDismissable = () => wrapper.findComponent('[data-testid=dismissable-checkbox]');
+ const findTargetRoles = () => wrapper.findComponent('[data-testid=target-roles-checkboxes]');
+ const findSubmitButton = () => wrapper.findComponent('[data-testid=submit-button]');
+ const findForm = () => wrapper.findComponent(GlForm);
+
+ function createComponent({ broadcastMessage = {}, glFeatures = {} }) {
+ wrapper = mount(MessageForm, {
+ provide: {
+ glFeatures,
+ targetAccessLevelOptions: MOCK_TARGET_ACCESS_LEVELS,
+ },
+ propsData: {
+ broadcastMessage: {
+ ...defaultProps,
+ ...broadcastMessage,
+ },
+ },
+ });
+ }
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ createAlert.mockClear();
+ });
+
+ describe('the message preview', () => {
+ it('renders the preview with the user selected theme', () => {
+ const theme = 'blue';
+ createComponent({ broadcastMessage: { theme } });
+ expect(findPreview().props().theme).toEqual(theme);
+ });
+
+ it('renders the placeholder text when the user message is blank', () => {
+ createComponent({ broadcastMessage: { message: ' ' } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.messagePlaceholder);
+ });
+ });
+
+ describe('theme select dropdown', () => {
+ it('renders for Banners', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } });
+ expect(findThemeSelect().exists()).toBe(true);
+ });
+
+ it('does not render for Notifications', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } });
+ expect(findThemeSelect().exists()).toBe(false);
+ });
+ });
+
+ describe('dismissable checkbox', () => {
+ it('renders for Banners', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_BANNER } });
+ expect(findDismissable().exists()).toBe(true);
+ });
+
+ it('does not render for Notifications', () => {
+ createComponent({ broadcastMessage: { broadcastType: TYPE_NOTIFICATION } });
+ expect(findDismissable().exists()).toBe(false);
+ });
+ });
+
+ describe('target roles checkboxes', () => {
+ it('renders when roleTargetedBroadcastMessages feature is enabled', () => {
+ createComponent({ glFeatures: { roleTargetedBroadcastMessages: true } });
+ expect(findTargetRoles().exists()).toBe(true);
+ });
+
+ it('does not render when roleTargetedBroadcastMessages feature is disabled', () => {
+ createComponent({ glFeatures: { roleTargetedBroadcastMessages: false } });
+ expect(findTargetRoles().exists()).toBe(false);
+ });
+ });
+
+ describe('form submit button', () => {
+ it('renders the "add" text when the message is not persisted', () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.add);
+ });
+
+ it('renders the "update" text when the message is persisted', () => {
+ createComponent({ broadcastMessage: { id: 100 } });
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.update);
+ });
+
+ it('is disabled when the user message is blank', () => {
+ createComponent({ broadcastMessage: { message: ' ' } });
+ expect(findSubmitButton().props().disabled).toBe(true);
+ });
+
+ it('is not disabled when the user message is present', () => {
+ createComponent({ broadcastMessage: { message: 'alsdjfkldsj' } });
+ expect(findSubmitButton().props().disabled).toBe(false);
+ });
+ });
+
+ describe('form submission', () => {
+ const defaultPayload = {
+ message: defaultProps.message,
+ broadcast_type: defaultProps.broadcastType,
+ theme: defaultProps.theme,
+ dismissable: defaultProps.dismissable,
+ target_path: defaultProps.targetPath,
+ target_access_levels: defaultProps.targetAccessLevels,
+ starts_at: defaultProps.startsAt,
+ ends_at: defaultProps.endsAt,
+ };
+
+ it('sends a create request for a new message form', async () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(1);
+ expect(axiosMock.history.post[0]).toMatchObject({
+ url: BROADCAST_MESSAGES_PATH,
+ data: JSON.stringify(defaultPayload),
+ });
+ });
+
+ it('shows an error alert if the create request fails', async () => {
+ createComponent({ broadcastMessage: { id: undefined } });
+ axiosMock.onPost(BROADCAST_MESSAGES_PATH).replyOnce(httpStatus.BAD_REQUEST);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: wrapper.vm.$options.i18n.addError,
+ }),
+ );
+ });
+
+ it('sends an update request for a persisted message form', async () => {
+ const id = 1337;
+ createComponent({ broadcastMessage: { id } });
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(axiosMock.history.patch).toHaveLength(1);
+ expect(axiosMock.history.patch[0]).toMatchObject({
+ url: `${BROADCAST_MESSAGES_PATH}/${id}`,
+ data: JSON.stringify(defaultPayload),
+ });
+ });
+
+ it('shows an error alert if the update request fails', async () => {
+ const id = 1337;
+ createComponent({ broadcastMessage: { id } });
+ axiosMock.onPost(`${BROADCAST_MESSAGES_PATH}/${id}`).replyOnce(httpStatus.BAD_REQUEST);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: wrapper.vm.$options.i18n.updateError,
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/mock_data.js b/spec/frontend/admin/broadcast_messages/mock_data.js
index 8dd98c2319d..2e20b5cf638 100644
--- a/spec/frontend/admin/broadcast_messages/mock_data.js
+++ b/spec/frontend/admin/broadcast_messages/mock_data.js
@@ -15,3 +15,11 @@ export const generateMockMessages = (n) =>
[...Array(n).keys()].map((id) => generateMockMessage(id + 1));
export const MOCK_MESSAGES = generateMockMessages(5).map((id) => generateMockMessage(id));
+
+export const MOCK_TARGET_ACCESS_LEVELS = [
+ ['Guest', 10],
+ ['Reporter', 20],
+ ['Developer', 30],
+ ['Maintainer', 40],
+ ['Owner', 50],
+];
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index abda13e0563..9084ecdb4cc 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -25,6 +25,7 @@ import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
@@ -277,6 +278,15 @@ describe('AdminRunnersApp', () => {
expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
});
+ it('Shows job status and links to jobs', () => {
+ const badge = wrapper
+ .find('tr [data-testid="td-summary"]')
+ .findComponent(RunnerJobStatusBadge);
+
+ expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
+ expect(badge.attributes('href')).toBe(`http://localhost/admin/runners/${id}#/jobs`);
+ });
+
it('When runner is paused or unpaused, some data is refetched', async () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 353c988c3db..10280c77303 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -166,14 +166,14 @@ describe('RunnerTypeCell', () => {
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
});
- it('Displays a custom slot', () => {
+ it.each(['runner-name', 'runner-job-status-badge'])('Displays a custom "%s" slot', (slotName) => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
- 'runner-name': slotContent,
+ [slotName]: slotContent,
},
},
);
diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
index 8cd3fb3cb4c..015bebf40e3 100644
--- a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -13,11 +13,12 @@ describe('RunnerTypeBadge', () => {
const findBadge = () => wrapper.findComponent(GlBadge);
- const createComponent = (props = {}) => {
+ const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(RunnerJobStatusBadge, {
propsData: {
...props,
},
+ ...options,
});
};
@@ -28,7 +29,7 @@ describe('RunnerTypeBadge', () => {
`(
'renders $jobStatus job status with "$text" text and styles',
({ jobStatus, classes, text }) => {
- createComponent({ jobStatus });
+ createComponent({ props: { jobStatus } });
expect(findBadge().props()).toMatchObject({ size: 'sm', variant: 'muted' });
expect(findBadge().classes().sort()).toEqual(classes.sort());
@@ -37,8 +38,14 @@ describe('RunnerTypeBadge', () => {
);
it('does not render an unknown status', () => {
- createComponent({ jobStatus: 'UNKNOWN_STATUS' });
+ createComponent({ props: { jobStatus: 'UNKNOWN_STATUS' } });
expect(wrapper.html()).toBe('');
});
+
+ it('adds arbitrary attributes', () => {
+ createComponent({ props: { jobStatus: JOB_STATUS_RUNNING }, attrs: { href: '/url' } });
+
+ expect(findBadge().attributes('href')).toBe('/url');
+ });
});
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index d53a0ce8f4f..1267d045623 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -188,6 +188,21 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
});
+ it('Render #runner-job-status-badge slot in "summary" cell', () => {
+ createComponent(
+ {
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
+ },
+ mountExtended,
+ );
+
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(
+ `Job status ${mockRunners[0].jobExecutionStatus}`,
+ );
+ });
+
it('Render #runner-actions-cell slot in "actions" cell', () => {
createComponent(
{
diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
index 0a8447c5d80..02b455d0b61 100644
--- a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
+++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js
@@ -1,4 +1,4 @@
-import { GlSearchBoxByType, GlCollapsibleListbox, GlButton } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
@@ -13,7 +13,6 @@ describe('AvailableAgentsDropdown', () => {
const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN;
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
- const findSearchInput = () => findDropdown().findComponent(GlSearchBoxByType);
const findCreateButton = () => wrapper.findComponent(GlButton);
const createWrapper = ({ propsData }) => {
@@ -48,7 +47,7 @@ describe('AvailableAgentsDropdown', () => {
});
it('renders only the agent searched for when the search item exists', async () => {
- findSearchInput().vm.$emit('input', searchAgentName);
+ findDropdown().vm.$emit('search', searchAgentName);
await nextTick();
expect(findDropdown().props('items')).toMatchObject([
@@ -63,7 +62,7 @@ describe('AvailableAgentsDropdown', () => {
${'is not rendered'} | ${''} | ${false}
${'is not rendered'} | ${searchAgentName} | ${false}
`('$condition when search is "$search"', async ({ search, createButtonRendered }) => {
- findSearchInput().vm.$emit('input', search);
+ findDropdown().vm.$emit('search', search);
await nextTick();
expect(findCreateButton().exists()).toBe(createButtonRendered);
@@ -87,7 +86,7 @@ describe('AvailableAgentsDropdown', () => {
describe('create new agent configuration', () => {
beforeEach(async () => {
- findSearchInput().vm.$emit('input', newAgentName);
+ findDropdown().vm.$emit('search', newAgentName);
await nextTick();
findCreateButton().vm.$emit('click');
});
@@ -103,9 +102,10 @@ describe('AvailableAgentsDropdown', () => {
describe('click enter to register new agent without configuration', () => {
beforeEach(async () => {
- findSearchInput().vm.$emit('input', newAgentName);
+ const dropdown = findDropdown();
+ dropdown.vm.$emit('search', newAgentName);
await nextTick();
- await findDropdown().trigger('keydown.enter');
+ await dropdown.trigger('keydown.enter');
});
it('emits agentSelected with the name of the clicked agent', () => {
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index 56eb6d75def..f853bb7b0a4 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -29,19 +29,13 @@ const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('SourceBranchDropdown', () => {
let wrapper;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownItemByText = (text) =>
- findAllDropdownItems().wrappers.find((item) => item.text() === text);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const assertDropdownItems = () => {
- const dropdownItems = findAllDropdownItems();
- expect(dropdownItems.wrappers).toHaveLength(mockProject.repository.branchNames.length);
- expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
- mockProject.repository.branchNames,
- );
+ const assertListboxItems = () => {
+ const listboxItems = findListbox().props('items');
+ expect(listboxItems).toHaveLength(mockProject.repository.branchNames.length);
+ expect(listboxItems.map((item) => item.text)).toEqual(mockProject.repository.branchNames);
};
function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
@@ -70,8 +64,8 @@ describe('SourceBranchDropdown', () => {
createComponent();
});
- it('sets dropdown `disabled` prop to `true`', () => {
- expect(findDropdown().props('disabled')).toBe(true);
+ it('sets listbox `disabled` prop to `true`', () => {
+ expect(findListbox().props('disabled')).toBe(true);
});
describe('when `selectedProject` becomes specified', () => {
@@ -83,29 +77,30 @@ describe('SourceBranchDropdown', () => {
await waitForPromises();
});
- it('sets dropdown props correctly', () => {
- expect(findDropdown().props()).toMatchObject({
- loading: false,
+ it('sets listbox props correctly', () => {
+ expect(findListbox().props()).toMatchObject({
disabled: false,
- text: 'Select a branch',
+ loading: false,
+ searchable: true,
+ searching: false,
+ toggleText: 'Select a branch',
});
});
- it('renders available source branches as dropdown items', () => {
- assertDropdownItems();
+ it('renders available source branches as listbox items', () => {
+ assertListboxItems();
});
});
});
describe('when `selectedProject` prop is specified', () => {
describe('when branches are loading', () => {
- it('renders loading icon in dropdown', () => {
+ it('sets loading prop to true', () => {
createComponent({
mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
props: { selectedProject: mockSelectedProject },
});
-
- expect(findLoadingIcon().isVisible()).toBe(true);
+ expect(findListbox().props('loading')).toEqual(true);
});
});
@@ -134,32 +129,32 @@ describe('SourceBranchDropdown', () => {
await waitForPromises();
});
- it('sets dropdown props correctly', () => {
- expect(findDropdown().props()).toMatchObject({
- loading: false,
+ it('sets listbox props correctly', () => {
+ expect(findListbox().props()).toMatchObject({
disabled: false,
- text: 'Select a branch',
+ loading: false,
+ searchable: true,
+ searching: false,
+ toggleText: 'Select a branch',
});
});
- it('omits monospace styling from dropdown', () => {
- expect(findDropdown().classes()).not.toContain('gl-font-monospace');
+ it('omits monospace styling from listbox', () => {
+ expect(findListbox().classes()).not.toContain('gl-font-monospace');
});
- it('renders available source branches as dropdown items', () => {
- assertDropdownItems();
+ it('renders available source branches as listbox items', () => {
+ assertListboxItems();
});
it("emits `change` event with the repository's `rootRef` by default", () => {
expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
});
- describe('when selecting a dropdown item', () => {
+ describe('when selecting a listbox item', () => {
it('emits `change` event with the selected branch name', async () => {
const mockBranchName = mockProject.repository.branchNames[1];
- const itemToSelect = findDropdownItemByText(mockBranchName);
- await itemToSelect.vm.$emit('click');
-
+ findListbox().vm.$emit('select', mockBranchName);
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
});
});
@@ -173,16 +168,12 @@ describe('SourceBranchDropdown', () => {
});
});
- it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
- expect(findDropdownItemByText(mockBranchName).props('isChecked')).toBe(true);
- });
-
- it('sets dropdown text to `selectedBranchName` value', () => {
- expect(findDropdown().props('text')).toBe(mockBranchName);
+ it('sets listbox text to `selectedBranchName` value', () => {
+ expect(findListbox().props('toggleText')).toBe(mockBranchName);
});
- it('adds monospace styling to dropdown', () => {
- expect(findDropdown().classes()).toContain('gl-font-monospace');
+ it('adds monospace styling to listbox', () => {
+ expect(findListbox().classes()).toContain('gl-font-monospace');
});
});
});
diff --git a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js b/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
index 795fae2f8d8..3fddbe7ae21 100644
--- a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
+++ b/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -16,6 +17,8 @@ function factory() {
});
}
+const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+
describe('Merge requests target project dropdown component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -67,7 +70,7 @@ describe('Merge requests target project dropdown component', () => {
await waitForPromises();
- wrapper.find('[data-testid="listbox-search-input"]').setValue('test');
+ findDropdown().vm.$emit('search', 'test');
jest.advanceTimersByTime(500);
await waitForPromises();
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
index 7fa8a18ea1f..b340a0b243f 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
@@ -63,15 +63,6 @@ describe('Pipeline Mini Graph', () => {
expect(findUpstreamArrowIcon().exists()).toBe(false);
expect(findDownstreamArrowIcon().exists()).toBe(false);
});
-
- it('triggers events in "action request complete"', () => {
- createComponent();
-
- findPipelineMiniGraph(0).vm.$emit('pipelineActionRequestComplete');
- findPipelineMiniGraph(1).vm.$emit('pipelineActionRequestComplete');
-
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
- });
});
describe('rendered state with upstream pipeline', () => {
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index 52b440f18bb..b7a9297d856 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -186,7 +186,7 @@ describe('Pipelines stage component', () => {
});
});
- describe('pipelineActionRequestComplete', () => {
+ describe('job update in dropdown', () => {
beforeEach(async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
@@ -204,24 +204,11 @@ describe('Pipelines stage component', () => {
await findCiActionBtn().trigger('click');
};
- it('closes dropdown when job item action is clicked', async () => {
- const hidden = jest.fn();
-
- wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
-
- expect(hidden).toHaveBeenCalledTimes(0);
-
- await clickCiAction();
- await waitForPromises();
-
- expect(hidden).toHaveBeenCalledTimes(1);
- });
-
- it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => {
+ it('keeps dropdown open when job item action is clicked', async () => {
await clickCiAction();
await waitForPromises();
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1);
+ expect(findDropdown().classes('show')).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index bfb780d5d39..7a71c1b54c6 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -39,15 +39,6 @@ describe('Pipeline Stages', () => {
expect(findPipelineStages()).toHaveLength(0);
});
- it('triggers events in "action request complete" in stages', () => {
- createComponent();
-
- findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete');
- findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete');
-
- expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2);
- });
-
it('update dropdown is false by default', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 044683ce533..740037a5ac8 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -17,7 +17,6 @@ import {
TRACKING_CATEGORIES,
} from '~/pipelines/constants';
-import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
jest.mock('~/pipelines/event_hub');
@@ -134,12 +133,6 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(0);
});
});
-
- it('when action request is complete, should refresh table', () => {
- findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
- });
});
describe('duration cell', () => {
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
new file mode 100644
index 00000000000..3335059554f
--- /dev/null
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -0,0 +1,22 @@
+import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const projectRootPath = 'root/Project1';
+const currentRef = 'main';
+const selectedRef = 'feature';
+
+describe('generateRefDestinationPath', () => {
+ it.each`
+ currentPath | result
+ ${projectRootPath} | ${`${projectRootPath}/-/tree/${selectedRef}`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1`} | ${`${projectRootPath}/-/tree/${selectedRef}/dir1`}
+ ${`${projectRootPath}/-/tree/${currentRef}/dir1/dir2`} | ${`${projectRootPath}/-/tree/${selectedRef}/dir1/dir2`}
+ ${`${projectRootPath}/-/blob/${currentRef}/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js`}
+ ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
+ `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 1421ddf85f4..f5155347a15 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -442,7 +442,10 @@ describe('Web IDE link component', () => {
{
showEditButton: false,
},
- { glFeatures: { vscodeWebIde: true }, userCalloutDismisserSlotProps: { dismiss } },
+ {
+ glFeatures: { vscodeWebIde: true },
+ userCalloutDismisserSlotProps: { dismiss },
+ },
);
});
it('does not skip the user_callout_dismisser query', () => {
@@ -514,12 +517,16 @@ describe('Web IDE link component', () => {
`(
'when vscode_web_ide=$featureFlag and showEditButton = $showEditButton',
({ vscodeWebIde, showEditButton }) => {
+ let dismiss;
+
beforeEach(() => {
+ dismiss = jest.fn();
+
createComponent(
{
showEditButton,
},
- { glFeatures: { vscodeWebIde } },
+ { glFeatures: { vscodeWebIde }, userCalloutDismisserSlotProps: { dismiss } },
);
});
@@ -534,6 +541,12 @@ describe('Web IDE link component', () => {
it('mounts new web ide callout popover', () => {
expect(findNewWebIdeCalloutPopover().exists()).toBe(false);
});
+
+ it('does not dismiss the callout when action button is clicked', () => {
+ findActionsButton().vm.$emit('actionClicked');
+
+ expect(dismiss).not.toHaveBeenCalled();
+ });
},
);
});
diff --git a/spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb
new file mode 100644
index 00000000000..9272689ef0b
--- /dev/null
+++ b/spec/graphql/resolvers/ci/runner_groups_resolver_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::RunnerGroupsResolver, feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:runner) { create(:ci_runner, :group, groups: [group1]) }
+
+ let(:args) { {} }
+
+ subject(:response) { resolve_groups(args) }
+
+ describe '#resolve' do
+ context 'with authorized user', :enable_admin_mode do
+ let(:current_user) { create(:user, :admin) }
+
+ it 'returns a lazy value with all groups' do
+ expect(response).to be_a(GraphQL::Execution::Lazy)
+ expect(response.value).to contain_exactly(group1)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ private
+
+ def resolve_groups(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: runner, args: args, ctx: context)
+ end
+end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 3d98591ac76..45864320115 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -1057,7 +1057,7 @@ RSpec.describe SearchHelper, feature_category: :global_search do
with_them do
it 'data item condition is set correctly' do
- @show_snippets = global_show_snippets
+ allow(search_service).to receive(:show_snippets?).and_return(global_show_snippets)
@project = global_project
expect(search_navigation[:snippet_titles][:condition]).to eq(condition)
@@ -1108,9 +1108,9 @@ RSpec.describe SearchHelper, feature_category: :global_search do
allow(self).to receive(:can?).and_return(true)
allow(self).to receive(:project_search_tabs?).and_return(true)
allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
- allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true)
allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
- @show_snippets = true
+ allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true)
+ allow(search_service).to receive(:show_snippets?).and_return(true)
@project = nil
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 9283c31a207..484b4702497 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
expect(subject).to be_from_ci_job_token
- expect(subject.ci_job_token_scope.source_project).to eq(job.project)
+ expect(subject.ci_job_token_scope.current_project).to eq(job.project)
end
end
@@ -100,7 +100,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
expect(subject).to be_from_ci_job_token
- expect(subject.ci_job_token_scope.source_project).to eq(job.project)
+ expect(subject.ci_job_token_scope.current_project).to eq(job.project)
end
else
it 'returns nil' do
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index 0cfd81216dc..7bba0775668 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
+ include ServiceDeskHelper
include_context 'email shared context'
before do
@@ -184,12 +185,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
context 'and template is present' do
let_it_be(:settings) { create(:service_desk_setting, project: project) }
- def set_template_file(file_name, content)
- file_path = ".gitlab/issue_templates/#{file_name}.md"
- project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
- settings.update!(issue_template_key: file_name)
- end
-
it 'appends template text to issue description' do
set_template_file('service_desk', 'text from template')
diff --git a/spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb b/spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb
new file mode 100644
index 00000000000..704999a99a9
--- /dev/null
+++ b/spec/lib/gitlab/github_gists_import/importer/gists_importer_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubGistsImport::Importer::GistsImporter, feature_category: :importer do
+ subject(:result) { described_class.new(user, token).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:client) { instance_double('Gitlab::GithubImport::Client', rate_limit_resets_in: 5) }
+ let(:token) { 'token' }
+ let(:page_counter) { instance_double('Gitlab::GithubImport::PageCounter', current: 1, set: true, expire!: true) }
+ let(:page) { instance_double('Gitlab::GithubImport::Client::Page', objects: [gist], number: 1) }
+ let(:url) { 'https://gist.github.com/foo/bar.git' }
+ let(:waiter) { Gitlab::JobWaiter.new(0, 'some-job-key') }
+
+ let(:gist) do
+ {
+ id: '055b70',
+ git_pull_url: url,
+ files: {
+ 'random.txt': {
+ filename: 'random.txt',
+ type: 'text/plain',
+ language: 'Text',
+ raw_url: 'https://gist.githubusercontent.com/user_name/055b70/raw/66a7be0d/random.txt',
+ size: 166903
+ }
+ },
+ public: false,
+ created_at: '2022-09-06T11:38:18Z',
+ updated_at: '2022-09-06T11:38:18Z',
+ description: 'random text'
+ }
+ end
+
+ let(:gist_hash) do
+ {
+ id: '055b70',
+ import_url: url,
+ files: {
+ 'random.txt': {
+ filename: 'random.txt',
+ type: 'text/plain',
+ language: 'Text',
+ raw_url: 'https://gist.githubusercontent.com/user_name/055b70/raw/66a7be0d/random.txt',
+ size: 166903
+ }
+ },
+ public: false,
+ created_at: '2022-09-06T11:38:18Z',
+ updated_at: '2022-09-06T11:38:18Z',
+ title: 'random text'
+ }
+ end
+
+ let(:gist_represent) { instance_double('Gitlab::GithubGistsImport::Representation::Gist', to_hash: gist_hash) }
+
+ describe '#execute' do
+ before do
+ allow(Gitlab::GithubImport::Client)
+ .to receive(:new)
+ .with(token, parallel: true)
+ .and_return(client)
+
+ allow(Gitlab::GithubImport::PageCounter)
+ .to receive(:new)
+ .with(user, :gists, 'github-gists-importer')
+ .and_return(page_counter)
+
+ allow(client)
+ .to receive(:each_page)
+ .with(:gists, nil, { page: 1 })
+ .and_yield(page)
+
+ allow(Gitlab::GithubGistsImport::Representation::Gist)
+ .to receive(:from_api_response)
+ .with(gist)
+ .and_return(gist_represent)
+
+ allow(Gitlab::JobWaiter)
+ .to receive(:new)
+ .and_return(waiter)
+ end
+
+ context 'when success' do
+ it 'spread parallel import' do
+ expect(Gitlab::GithubGistsImport::ImportGistWorker)
+ .to receive(:bulk_perform_in)
+ .with(
+ 1.second,
+ [[user.id, gist_hash, waiter.key]],
+ batch_delay: 1.minute,
+ batch_size: 1000
+ )
+
+ expect(result.waiter).to be_an_instance_of(Gitlab::JobWaiter)
+ expect(result.waiter.jobs_remaining).to eq(1)
+ end
+ end
+
+ context 'when failure' do
+ it 'returns an error' do
+ expect(Gitlab::GithubGistsImport::ImportGistWorker)
+ .to receive(:bulk_perform_in)
+ .and_raise(StandardError, 'Error Message')
+
+ expect(result.error).to be_an_instance_of(StandardError)
+ end
+ end
+
+ context 'when rate limit reached' do
+ it 'returns an error' do
+ expect(Gitlab::GithubGistsImport::ImportGistWorker)
+ .to receive(:bulk_perform_in)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
+
+ expect(result.error).to be_an_instance_of(Gitlab::GithubImport::RateLimitError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb b/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb
index f36fbc637d0..480aefb2c74 100644
--- a/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb
+++ b/spec/lib/gitlab/github_gists_import/representation/gist_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubGistsImport::Representation::Gist do
+RSpec.describe Gitlab::GithubGistsImport::Representation::Gist, feature_category: :importer do
shared_examples 'a Gist' do
it 'returns an instance of Gist' do
expect(gist).to be_an_instance_of(described_class)
diff --git a/spec/lib/gitlab/github_import/page_counter_spec.rb b/spec/lib/gitlab/github_import/page_counter_spec.rb
index 568bc8cbbef..511b19c00e5 100644
--- a/spec/lib/gitlab/github_import/page_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/page_counter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
+RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache, feature_category: :importer do
let(:project) { double(:project, id: 1) }
let(:counter) { described_class.new(project, :issues) }
@@ -16,6 +16,16 @@ RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
expect(described_class.new(project, :issues).current).to eq(2)
end
+
+ context 'when gists import' do
+ let(:user) { instance_double('User', id: 2) }
+
+ it 'uses gists specific key' do
+ result = described_class.new(user, :gists, 'github-gists-importer')
+
+ expect(result.cache_key).to eq('github-gists-importer/page-counter/2/gists')
+ end
+ end
end
describe '#set' do
diff --git a/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
index 5858986dfc8..afcfb063af3 100644
--- a/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
+++ b/spec/lib/gitlab/graphql/limit/field_call_count_spec.rb
@@ -36,6 +36,15 @@ RSpec.describe Gitlab::Graphql::Limit::FieldCallCount do
expect(resolve_value).to be_an_instance_of(Gitlab::Graphql::Errors::LimitError)
end
+ it 'does not return an error when the field is called multiple times in separte queries' do
+ query_1 = GraphQL::Query.new(GitlabSchema)
+ query_2 = GraphQL::Query.new(GitlabSchema)
+
+ resolve_field(field, { value: 'foo' }, object_type: owner, query: query_1)
+
+ expect { resolve_field(field, { value: 'foo' }, object_type: owner, query: query_2) }.not_to raise_error
+ end
+
context 'when limit is not specified' do
let(:field) do
::Types::BaseField.new(name: 'value', type: GraphQL::Types::String, null: true, owner: owner) do
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
index f3c95332ca0..79a716c2087 100644
--- a/spec/models/badge_spec.rb
+++ b/spec/models/badge_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Badge do
- let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{project_name}/%{default_branch}/%{commit_sha}/%{project_title}' }
describe 'validations' do
# Requires the let variable url_sym
@@ -64,7 +64,7 @@ RSpec.describe Badge do
it 'uses the project information to populate the url placeholders' do
stub_project_commit_info(project)
- expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
+ expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/#{project.path}/master/whatever/#{project.title}"
end
it 'returns the url if the project used is nil' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 39ea3fafec3..70c8a55c2f5 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -5647,4 +5647,33 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
expect(ci_build.job_variables.second.partition_id).to eq(ci_testing_partition_id)
end
end
+
+ describe 'assigning token', :ci_partitionable do
+ include Ci::PartitioningHelpers
+
+ let(:new_pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { create(:ci_build, pipeline: new_pipeline) }
+
+ before do
+ stub_current_partition_id
+ end
+
+ it 'includes partition_id as a token prefix' do
+ prefix = ci_build.token.split('_').first.to_i(16)
+
+ expect(prefix).to eq(ci_testing_partition_id)
+ end
+
+ context 'when ci_build_partition_id_token_prefix is disabled' do
+ before do
+ stub_feature_flags(ci_build_partition_id_token_prefix: false)
+ end
+
+ it 'does not include partition_id as a token prefix' do
+ prefix = ci_build.token.split('_').first.to_i(16)
+
+ expect(prefix).not_to eq(ci_testing_partition_id)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb
new file mode 100644
index 00000000000..45083d64393
--- /dev/null
+++ b/spec/models/ci/job_token/allowlist_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:source_project) { create(:project) }
+
+ let(:allowlist) { described_class.new(source_project, direction: direction) }
+ let(:direction) { :outbound }
+
+ describe '#projects' do
+ subject(:projects) { allowlist.projects }
+
+ context 'when no projects are added to the scope' do
+ [:inbound, :outbound].each do |d|
+ let(:direction) { d }
+
+ it 'returns the project defining the scope' do
+ expect(projects).to contain_exactly(source_project)
+ end
+ end
+ end
+
+ context 'when projects are added to the scope' do
+ include_context 'with scoped projects'
+
+ where(:direction, :additional_project) do
+ :outbound | ref(:outbound_scoped_project)
+ :inbound | ref(:inbound_scoped_project)
+ end
+
+ with_them do
+ it 'returns all projects that can be accessed from a given scope' do
+ expect(projects).to contain_exactly(source_project, additional_project)
+ end
+ end
+ end
+ end
+
+ describe '#includes?' do
+ subject { allowlist.includes?(includes_project) }
+
+ context 'without scoped projects' do
+ let(:unscoped_project) { build(:project) }
+
+ where(:includes_project, :direction, :result) do
+ ref(:source_project) | :outbound | false
+ ref(:source_project) | :inbound | false
+ ref(:unscoped_project) | :outbound | false
+ ref(:unscoped_project) | :inbound | false
+ end
+
+ with_them do
+ it { is_expected.to be result }
+ end
+ end
+
+ context 'with scoped projects' do
+ include_context 'with scoped projects'
+
+ where(:includes_project, :direction, :result) do
+ ref(:source_project) | :outbound | false
+ ref(:source_project) | :inbound | false
+ ref(:inbound_scoped_project) | :outbound | false
+ ref(:inbound_scoped_project) | :inbound | true
+ ref(:outbound_scoped_project) | :outbound | true
+ ref(:outbound_scoped_project) | :inbound | false
+ ref(:unscoped_project1) | :outbound | false
+ ref(:unscoped_project1) | :inbound | false
+ ref(:unscoped_project2) | :outbound | false
+ ref(:unscoped_project2) | :inbound | false
+ end
+
+ with_them do
+ it { is_expected.to be result }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
index 6f428d1aa8b..91491733c44 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobToken::ProjectScopeLink do
+RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
@@ -50,8 +50,8 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
end
end
- describe '.from_project' do
- subject { described_class.from_project(project) }
+ describe '.with_source' do
+ subject { described_class.with_source(project) }
let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
@@ -61,8 +61,8 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
end
end
- describe '.to_project' do
- subject { described_class.to_project(project) }
+ describe '.with_target' do
+ subject { described_class.with_target(project) }
let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 1e3f6d044d2..37c56973506 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -2,58 +2,72 @@
require 'spec_helper'
-RSpec.describe Ci::JobToken::Scope do
- let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) }
+RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do
+ let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) }
- let(:scope) { described_class.new(project) }
+ let(:scope) { described_class.new(source_project) }
describe '#all_projects' do
subject(:all_projects) { scope.all_projects }
context 'when no projects are added to the scope' do
it 'returns the project defining the scope' do
- expect(all_projects).to contain_exactly(project)
+ expect(all_projects).to contain_exactly(source_project)
end
end
- context 'when other projects are added to the scope' do
- let_it_be(:scoped_project) { create(:project) }
- let_it_be(:unscoped_project) { create(:project) }
-
- let!(:link_in_scope) { create(:ci_job_token_project_scope_link, source_project: project, target_project: scoped_project) }
- let!(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project) }
+ context 'when projects are added to the scope' do
+ include_context 'with scoped projects'
it 'returns all projects that can be accessed from a given scope' do
- expect(subject).to contain_exactly(project, scoped_project)
+ expect(subject).to contain_exactly(source_project, outbound_scoped_project)
end
end
end
- describe '#includes?' do
- subject { scope.includes?(target_project) }
+ describe '#allows?' do
+ subject { scope.allows?(includes_project) }
- context 'when param is the project defining the scope' do
- let(:target_project) { project }
+ context 'without scoped projects' do
+ context 'when self referential' do
+ let(:includes_project) { source_project }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_truthy }
+ end
end
- context 'when param is a project in scope' do
- let(:target_link) { create(:ci_job_token_project_scope_link, source_project: project) }
- let(:target_project) { target_link.target_project }
+ context 'with scoped projects' do
+ include_context 'with scoped projects'
- it { is_expected.to be_truthy }
- end
+ context 'when project is in outbound scope' do
+ let(:includes_project) { outbound_scoped_project }
- context 'when param is a project in another scope' do
- let(:scope_link) { create(:ci_job_token_project_scope_link) }
- let(:target_project) { scope_link.target_project }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when project is in inbound scope' do
+ let(:includes_project) { inbound_scoped_project }
+
+ it { is_expected.to be_falsey }
+ end
- it { is_expected.to be_falsey }
+ context 'when project is linked to a different project' do
+ let(:includes_project) { unscoped_project1 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when project is unlinked to a project' do
+ let(:includes_project) { unscoped_project2 }
+
+ it { is_expected.to be_falsey }
+ end
context 'when project scope setting is disabled' do
+ let(:includes_project) { unscoped_project1 }
+
before do
- project.ci_outbound_job_token_scope_enabled = false
+ source_project.ci_outbound_job_token_scope_enabled = false
end
it 'considers any project to be part of the scope' do
diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb
index 2d1fe11147c..6cbb151d703 100644
--- a/spec/models/ci/runner_namespace_spec.rb
+++ b/spec/models/ci/runner_namespace_spec.rb
@@ -12,4 +12,27 @@ RSpec.describe Ci::RunnerNamespace do
let!(:parent) { model.namespace }
end
+
+ describe '.for_runner' do
+ subject(:for_runner) { described_class.for_runner(runner_ids) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:runners) { create_list(:ci_runner, 3, :group, groups: [group]) }
+
+ context 'with runner ids' do
+ let(:runner_ids) { runners[1..2].map(&:id) }
+
+ it 'returns requested runner namespaces' do
+ is_expected.to eq(runners[1..2].flat_map(&:runner_namespaces))
+ end
+ end
+
+ context 'with runners' do
+ let(:runner_ids) { runners.first }
+
+ it 'returns requested runner namespaces' do
+ is_expected.to eq(runners.first.runner_namespaces)
+ end
+ end
+ end
end
diff --git a/spec/presenters/search_service_presenter_spec.rb b/spec/presenters/search_service_presenter_spec.rb
index af9fee8cfd9..a235f954366 100644
--- a/spec/presenters/search_service_presenter_spec.rb
+++ b/spec/presenters/search_service_presenter_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SearchServicePresenter do
+RSpec.describe SearchServicePresenter, feature_category: :global_search do
let(:user) { create(:user) }
let(:search) { '' }
let(:search_service) { SearchService.new(user, search: search, scope: scope) }
@@ -51,4 +51,10 @@ RSpec.describe SearchServicePresenter do
it { expect(presenter.show_results_status?).to eq(result) }
end
end
+
+ describe '#advanced_search_enabled?' do
+ let(:scope) { nil }
+
+ it { expect(presenter.advanced_search_enabled?).to eq(false) }
+ end
end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 30de5ed1ede..7a1dc614dcf 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -367,4 +367,65 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
expect_graphql_errors_to_include [/"jobs" field can be requested only for 1 Project\(s\) at a time./]
end
end
+
+ context 'when batched querying jobs for multiple projects' do
+ let(:batched) do
+ [
+ { query: query_1 },
+ { query: query_2 }
+ ]
+ end
+
+ let(:query_1) do
+ %(
+ query Page1 {
+ projects {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:query_2) do
+ %(
+ query Page2 {
+ projects {
+ nodes {
+ jobs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ create_list(:project, 2).each do |project|
+ project.add_developer(user)
+ create(:ci_build, project: project)
+ end
+ end
+
+ it 'limits the specific field evaluation per query' do
+ get_multiplex(batched, current_user: user)
+
+ resp = json_response
+
+ expect(resp.first.dig('data', 'projects', 'nodes').first.dig('jobs', 'nodes').first['name']).to eq('test')
+ expect(resp.first['errors'].first['message'])
+ .to match(/"jobs" field can be requested only for 1 Project\(s\) at a time./)
+ expect(resp.second.dig('data', 'projects', 'nodes').first.dig('jobs', 'nodes').first['name']).to eq('test')
+ expect(resp.second['errors'].first['message'])
+ .to match(/"jobs" field can be requested only for 1 Project\(s\) at a time./)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 1b3241c9959..2e840096814 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -484,6 +484,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
groups {
nodes {
id
+ path
+ fullPath
+ webUrl
}
}
projects {
@@ -496,6 +499,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
}
ownerProject {
id
+ path
+ fullPath
+ webUrl
}
}
SINGLE
@@ -505,7 +511,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:active_group_runner2) { create(:ci_runner, :group) }
# Currently excluding known N+1 issues, see https://gitlab.com/gitlab-org/gitlab/-/issues/334759
- let(:excluded_fields) { %w[jobCount groups projects ownerProject] }
+ let(:excluded_fields) { %w[jobCount jobs groups projects ownerProject] }
let(:single_query) do
<<~QUERY
@@ -567,6 +573,72 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
+ describe 'Query limits with jobs' do
+ let!(:group1) { create(:group) }
+ let!(:group2) { create(:group) }
+ let!(:project1) { create(:project, :repository, group: group1) }
+ let!(:project2) { create(:project, :repository, group: group1) }
+ let!(:project3) { create(:project, :repository, group: group2) }
+
+ let!(:pipeline1) { create(:ci_pipeline, project: project1) }
+ let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) }
+ let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
+
+ let(:query) do
+ <<~QUERY
+ {
+ runner(id: "#{project_runner2.to_global_id}") {
+ id
+ jobs {
+ nodes {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ group
+ icon
+ text
+ }
+ shortSha
+ commitPath
+ finishedAt
+ duration
+ queuedDuration
+ tags
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ it 'does not execute more queries per job', :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+
+ control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) }
+
+ # Add a new build to project_runner2
+ project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3)
+ pipeline2 = create(:ci_pipeline, project: project3)
+ build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2)
+
+ args[:current_user] = create(:user, :admin) # do not reuse same user
+ expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+
+ expect(graphql_data.count).to eq 1
+ expect(graphql_data).to match(
+ a_hash_including(
+ 'runner' => a_graphql_entity_for(
+ project_runner2,
+ jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) }
+ )
+ ))
+ end
+ end
+
describe 'sorting and pagination' do
let(:query) do
<<~GQL
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index 9e2b043f889..490716ddbe2 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::Scope.new(project).includes?(target_project) }.from(false).to(true)
+ end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(false).to(true)
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
index 5d57b1f0872..607c6bd85c2 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/remove_project_spec.rb
@@ -66,7 +66,7 @@ RSpec.describe 'CiJobTokenScopeRemoveProject', feature_category: :continuous_int
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::Scope.new(project).includes?(target_project) }.from(true).to(false)
+ end.to change { Ci::JobToken::Scope.new(project).allows?(target_project) }.from(true).to(false)
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
new file mode 100644
index 00000000000..0e43fa024f3
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_play_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineSchedulePlay', feature_category: :continuious_integration do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline_schedule) do
+ create(
+ :ci_pipeline_schedule,
+ :every_minute,
+ project: project,
+ owner: user
+ )
+ end
+
+ let(:mutation) do
+ graphql_mutation(
+ :pipeline_schedule_play,
+ { id: pipeline_schedule.to_global_id.to_s },
+ <<-QL
+ pipelineSchedule { id, nextRunAt }
+ errors
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:pipeline_schedule_play) }
+
+ context 'when unauthorized' do
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ expect(graphql_errors[0]['message'])
+ .to eq(
+ "The resource that you are attempting to access does not exist " \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ project.add_maintainer(user)
+ pipeline_schedule.update_columns(next_run_at: 2.hours.ago)
+ end
+
+ context 'when mutation succeeds' do
+ it do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(mutation_response['pipelineSchedule']['id']).to include(pipeline_schedule.id.to_s)
+ new_next_run_at = DateTime.parse(mutation_response['pipelineSchedule']['nextRunAt'])
+ expect(new_next_run_at).not_to eq(pipeline_schedule.next_run_at)
+ expect(new_next_run_at).to eq(pipeline_schedule.reset.next_run_at)
+ expect(mutation_response['errors']).to eq([])
+ end
+ end
+
+ context 'when mutation fails' do
+ before do
+ allow(RunPipelineScheduleWorker).to receive(:perform_async).and_return(nil)
+ end
+
+ it do
+ expect(RunPipelineScheduleWorker)
+ .to receive(:perform_async)
+ .with(pipeline_schedule.id, user.id)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(mutation_response['pipelineSchedule']).to be_nil
+ expect(mutation_response['errors']).to match_array(['Unable to schedule a pipeline to run immediately.'])
+ end
+ end
+ end
+end
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index b8a11c04a16..cd1d09da16d 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -79,15 +79,25 @@ feature_category: :user_management do
end
context 'when the user is signing in from an unknown ip address' do
+ let(:ip_check_enabled) { true }
+
before do
+ stub_feature_flags(check_ip_address_for_email_verification: ip_check_enabled)
allow(AuthenticationEvent)
.to receive(:initial_login_or_known_ip_address?)
.and_return(false)
+
perform_enqueued_jobs { sign_in }
end
it_behaves_like 'send verification instructions'
it_behaves_like 'prompt for email verification'
+
+ context 'when the check_ip_address_for_email_verification feature flag is disabled' do
+ let(:ip_check_enabled) { false }
+
+ it_behaves_like 'not verifying with email'
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/logger_spec.rb b/spec/services/ci/create_pipeline_service/logger_spec.rb
index 76155690ced..ccb15bfa684 100644
--- a/spec/services/ci/create_pipeline_service/logger_spec.rb
+++ b/spec/services/ci/create_pipeline_service/logger_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do # rubocop: disable RSpec/FilePath
+RSpec.describe Ci::CreatePipelineService, # rubocop: disable RSpec/FilePath
+ :yaml_processor_feature_flag_corectness,
+ feature_category: :continuous_integration do
describe 'pipeline logger' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -32,6 +34,9 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
'pipeline_size_count' => a_kind_of(Numeric),
'pipeline_step_gitlab_ci_pipeline_chain_seed_duration_s' => a_kind_of(Numeric),
'pipeline_seed_build_inclusion_duration_s' => counters,
+ 'pipeline_seed_build_errors_duration_s' => counters,
+ 'pipeline_seed_build_to_resource_duration_s' => counters,
+ 'pipeline_seed_stage_seeds_duration_s' => counters,
'pipeline_builds_tags_count' => a_kind_of(Numeric),
'pipeline_builds_distinct_tags_count' => a_kind_of(Numeric)
}
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 8eb3b777346..f42ab198a04 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -922,27 +922,6 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
- it_behaves_like 'measurable service' do
- before do
- opts.merge!(
- current_user: user,
- path: 'foo'
- )
- end
-
- let(:base_log_data) do
- {
- class: Projects::CreateService.name,
- current_user: user.name,
- project_full_path: "#{user.namespace.full_path}/#{opts[:path]}"
- }
- end
-
- after do
- create_project(user, opts)
- end
- end
-
context 'with specialized project_authorization workers' do
let_it_be(:other_user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 68456c1554d..2c1ebe27014 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -220,20 +220,5 @@ RSpec.describe Projects::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
-
- it_behaves_like 'measurable service' do
- let(:base_log_data) do
- {
- class: described_class.name,
- current_user: user.name,
- project_full_path: project.full_path,
- file_path: shared.export_path
- }
- end
-
- after do
- service.execute(after_export_strategy)
- end
- end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index b3f8980a7bd..bb11b2e617e 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -419,25 +419,5 @@ RSpec.describe Projects::ImportService do
end
end
end
-
- it_behaves_like 'measurable service' do
- let(:base_log_data) do
- {
- class: described_class.name,
- current_user: user.name,
- project_full_path: project.full_path,
- import_type: project.import_type,
- file_path: project.import_source
- }
- end
-
- before do
- project.import_type = 'github'
- end
-
- after do
- subject.execute
- end
- end
end
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 26def474b88..90e80a45515 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SearchService do
+RSpec.describe SearchService, feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:accessible_group) { create(:group, :private) }
diff --git a/spec/support/helpers/project_template_test_helper.rb b/spec/support/helpers/project_template_test_helper.rb
index bd2fd367fae..bedbb8601e8 100644
--- a/spec/support/helpers/project_template_test_helper.rb
+++ b/spec/support/helpers/project_template_test_helper.rb
@@ -9,7 +9,7 @@ module ProjectTemplateTestHelper
nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx
serverless_framework tencent_serverless_framework
jsonnet cluster_management kotlin_native_linux
- pelican bridgetown
+ pelican bridgetown typo3_distribution
]
end
end
diff --git a/spec/support/helpers/service_desk_helper.rb b/spec/support/helpers/service_desk_helper.rb
new file mode 100644
index 00000000000..d67ee5b8a11
--- /dev/null
+++ b/spec/support/helpers/service_desk_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ServiceDeskHelper
+ def set_template_file(file_name, content)
+ file_path = ".gitlab/issue_templates/#{file_name}.md"
+ project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
+ settings.update!(issue_template_key: file_name)
+ end
+end
diff --git a/spec/support/shared_contexts/models/ci/job_token_scope.rb b/spec/support/shared_contexts/models/ci/job_token_scope.rb
new file mode 100644
index 00000000000..51f671b139d
--- /dev/null
+++ b/spec/support/shared_contexts/models/ci/job_token_scope.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with scoped projects' do
+ let_it_be(:inbound_scoped_project) { create_scoped_project(source_project, direction: :inbound) }
+ let_it_be(:outbound_scoped_project) { create_scoped_project(source_project, direction: :outbound) }
+ let_it_be(:unscoped_project1) { create(:project) }
+ let_it_be(:unscoped_project2) { create(:project) }
+
+ let_it_be(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project1) }
+
+ def create_scoped_project(source_project, direction:)
+ create(:project).tap do |scoped_project|
+ create(
+ :ci_job_token_project_scope_link,
+ source_project: source_project,
+ target_project: scoped_project,
+ direction: direction
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
index cc74c977064..b73f40ff28c 100644
--- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
+++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
@@ -6,7 +6,7 @@ RSpec.shared_examples 'search timeouts' do |scope|
context 'when search times out' do
before do
allow_next_instance_of(SearchService) do |service|
- allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled)
+ allow(service).to receive(:search_results).and_raise(ActiveRecord::QueryCanceled)
end
visit(search_path(search: 'test', scope: scope, **additional_params))
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 62a52bcf83f..5a1ae715f8f 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'projects/tree/show' do
it 'displays correctly' do
render
- expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
+ expect(rendered).to have_css('#js-tree-ref-switcher')
end
end
end
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 5f9c6c65a08..26ec2c6ae74 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -2,27 +2,32 @@
require 'spec_helper'
-RSpec.describe 'search/show' do
+RSpec.describe 'search/show', feature_category: :global_search do
let(:search_term) { nil }
let(:user) { build(:user) }
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: false, advanced_search_enabled?: false)
+ end
before do
stub_template "search/_category.html.haml" => 'Category Partial'
stub_template "search/_results.html.haml" => 'Results Partial'
+
+ assign(:search_service, search_service_presenter)
end
context 'search_page_vertical_nav feature flag enabled' do
before do
allow(view).to receive(:current_user) { user }
assign(:search_term, search_term)
-
- render
end
context 'when search term is supplied' do
let(:search_term) { 'Search Foo' }
it 'will not render category partial' do
+ render
+
expect(rendered).not_to render_template('search/_category')
expect(rendered).to render_template('search/_results')
end
@@ -34,17 +39,19 @@ RSpec.describe 'search/show' do
stub_feature_flags(search_page_vertical_nav: false)
assign(:search_term, search_term)
-
- render
end
context 'when the search page is opened' do
it 'displays the title' do
+ render
+
expect(rendered).to have_selector('h1.page-title', text: 'Search')
expect(rendered).not_to have_selector('h1.page-title code')
end
it 'does not render partials' do
+ render
+
expect(rendered).not_to render_template('search/_category')
expect(rendered).not_to render_template('search/_results')
end
@@ -54,6 +61,8 @@ RSpec.describe 'search/show' do
let(:search_term) { 'Search Foo' }
it 'renders partials' do
+ render
+
expect(rendered).to render_template('search/_category')
expect(rendered).to render_template('search/_results')
end
@@ -73,8 +82,8 @@ RSpec.describe 'search/show' do
end
context 'search with full count' do
- before do
- assign(:without_count, false)
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: false, advanced_search_enabled?: false)
end
it 'renders meta tags for a group' do
@@ -96,8 +105,8 @@ RSpec.describe 'search/show' do
end
context 'search without full count' do
- before do
- assign(:without_count, true)
+ let(:search_service_presenter) do
+ instance_double(SearchServicePresenter, without_count?: true, advanced_search_enabled?: false)
end
it 'renders meta tags for a group' do
diff --git a/vendor/project_templates/typo3_distribution.tar.gz b/vendor/project_templates/typo3_distribution.tar.gz
new file mode 100644
index 00000000000..13c158d1462
--- /dev/null
+++ b/vendor/project_templates/typo3_distribution.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 46102aa75dd..368e12f1287 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1136,10 +1136,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.13.0.tgz#2d62286c956bd49ba7156b2aa4eed79507baca53"
integrity sha512-Yv4dZ4pOyUVMCZXNxLuMinZ/x8E6+g8/yM1z/2ERT0t7hSAC3bCUHn2OEFpujtYzFtwMZXMFPQFEJJipQ1I/+w==
-"@gitlab/ui@52.0.0":
- version "52.0.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-52.0.0.tgz#dfd1c0848a72fd872b1923567352c80b59afb0e2"
- integrity sha512-vhjztkUc1Dol3sWVNNdpDWMuvj/TDCOnQZxSE2cXSYAeSdteUMrBLu9VdkIpIDoyPltDkGhtinzi5RaU1EVoqw==
+"@gitlab/ui@52.3.0":
+ version "52.3.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-52.3.0.tgz#8495b86dceecdf150f9c4a577353791b61f1a3fd"
+ integrity sha512-vYjHt61MQTiMh1eP72+iSABdg8DhbKk+cZBkjg79ACwJjh9zsJl7DVBFNWF37FS+21/errepTybQzxGbzOvwxA==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"