diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 18:09:27 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-13 18:09:27 +0000 |
commit | 5248c5e2212b8e42b28b23e6839d69db0006829b (patch) | |
tree | f989d4b4cd06fc5dc28c024a5f230b42b0af179b | |
parent | 0d55697d64b5f053bbd0f69da2962e7478097de3 (diff) | |
download | gitlab-ce-5248c5e2212b8e42b28b23e6839d69db0006829b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 Binary files differdeleted file mode 100644 index 927b4f812a5..00000000000 --- a/doc/user/project/merge_requests/reviews/img/suggestion_code_block_editor_v12_8.png +++ /dev/null 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 Binary files differnew file mode 100644 index 00000000000..13c158d1462 --- /dev/null +++ b/vendor/project_templates/typo3_distribution.tar.gz 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" |