Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-04 18:07:57 +00:00
parent 31f5ca749f
commit 0985362048
112 changed files with 5054 additions and 5945 deletions

View File

@ -288,14 +288,6 @@ export default {
'app/assets/javascripts/webhooks/components/form_custom_header_item.vue',
'app/assets/javascripts/work_items/components/create_work_item.vue',
'app/assets/javascripts/work_items/components/design_management/design_notes/design_discussion.vue',
'app/assets/javascripts/work_items/components/notes/system_note.vue',
'app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue',
'app/assets/javascripts/work_items/components/notes/work_item_add_note.vue',
'app/assets/javascripts/work_items/components/notes/work_item_comment_locked.vue',
'app/assets/javascripts/work_items/components/notes/work_item_note.vue',
'app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue',
'app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue',
'app/assets/javascripts/work_items/components/notes/work_item_note_body.vue',
'app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue',
'app/assets/javascripts/work_items/components/shared/work_item_token_input.vue',
'app/assets/javascripts/work_items/components/work_item_assignees.vue',
@ -318,7 +310,6 @@ export default {
'app/assets/javascripts/work_items/components/work_item_links/work_item_rolled_up_data.vue',
'app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue',
'app/assets/javascripts/work_items/components/work_item_milestone.vue',
'app/assets/javascripts/work_items/components/work_item_notes.vue',
'app/assets/javascripts/work_items/components/work_item_notifications_widget.vue',
'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue',
'app/assets/javascripts/work_items/components/work_item_state_toggle.vue',

View File

@ -87,7 +87,6 @@ stages:
- .docker-in-docker
- .qa-install
- .e2e-test-variables
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}-rust-${RUST_VERSION}:git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-kubectl-${KUBECTL_VERSION}-helm-${HELM_VERSION}-kind-${KIND_VERSION}"
variables:
# variables related to failure issue reporting
# default values from /ci/qa-report.gitlab-ci.yml will work with gitlab-qa orchestrator but not with cng and gdk tests

View File

@ -30,6 +30,7 @@ workflow:
- export QA_GITLAB_URL="http://gitlab.${GITLAB_DOMAIN}"
.cng-test:
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ci/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-kubectl-${KUBECTL_VERSION}-helm-${HELM_VERSION}-kind-${KIND_VERSION}"
stage: test
extends:
- .e2e-test-base

View File

@ -28,6 +28,7 @@ workflow:
- mv $CI_BUILDS_DIR/*.log $CI_PROJECT_DIR/
.gdk-qa-base:
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ci/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}"
extends:
- .e2e-test-base
# ignore runtime data from gdk because it's significantly slower than cng and runtime data for

View File

@ -1 +1 @@
5ddaf4012bca2e5291f21503c5df3ccd7bafe62f
cdfcfafe0ef01938ccb97298d64245e6a5f0fbf1

View File

@ -1,12 +1,11 @@
<script>
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
export default {
name: 'InputsAdoptionBanner',
components: { GlAlert, GlSprintf, UserCalloutDismisser },
components: { GlAlert, GlButton, GlSprintf, UserCalloutDismisser },
inputsDocsPath: helpPagePath('ci/yaml/inputs'),
inject: ['canViewPipelineEditor', 'pipelineEditorPath'],
props: {
@ -19,16 +18,6 @@ export default {
showPipelineEditorButton() {
return this.canViewPipelineEditor && this.pipelineEditorPath;
},
alertProps() {
return {
secondaryButtonText: __('Learn more'),
secondaryButtonLink: this.$options.inputsDocsPath,
...(this.showPipelineEditorButton && {
primaryButtonText: s__('Pipelines|Go to the pipeline editor'),
primaryButtonLink: this.pipelineEditorPath,
}),
};
},
},
};
</script>
@ -36,13 +25,7 @@ export default {
<template>
<user-callout-dismisser :feature-name="featureName">
<template #default="{ dismiss, shouldShowCallout }">
<gl-alert
v-if="shouldShowCallout"
variant="tip"
class="gl-my-4"
v-bind="alertProps"
@dismiss="dismiss"
>
<gl-alert v-if="shouldShowCallout" variant="tip" class="gl-my-4" @dismiss="dismiss">
<gl-sprintf
:message="
s__(
@ -54,6 +37,19 @@ export default {
<code>{{ content }}</code>
</template>
</gl-sprintf>
<div class="gl-mt-4 gl-flex gl-gap-3">
<gl-button
v-if="showPipelineEditorButton"
:href="pipelineEditorPath"
category="secondary"
variant="confirm"
>
{{ __('Go to the pipeline editor') }}
</gl-button>
<gl-button :href="$options.inputsDocsPath" category="secondary" target="_blank">
{{ __('Learn more') }}
</gl-button>
</div>
</gl-alert>
</template>
</user-callout-dismisser>

View File

@ -7,6 +7,8 @@ import InputsTableSkeletonLoader from './pipeline_inputs_table/inputs_table_skel
import PipelineInputsTable from './pipeline_inputs_table/pipeline_inputs_table.vue';
import getPipelineInputsQuery from './graphql/queries/pipeline_creation_inputs.query.graphql';
const ARRAY_TYPE = 'ARRAY';
export default {
name: 'PipelineInputsForm',
components: {
@ -78,10 +80,21 @@ export default {
input.name === updatedInput.name ? updatedInput : input,
);
const nameValuePairs = this.inputs.map((input) => ({
name: input.name,
value: input.default,
}));
const nameValuePairs = this.inputs.map((input) => {
let value = input.default;
// Convert string to array for ARRAY type inputs
if (input.type === ARRAY_TYPE && typeof value === 'string' && value) {
try {
value = JSON.parse(value);
if (!Array.isArray(value)) value = [value];
} catch (e) {
value = value.split(',').map((item) => item.trim());
}
}
return { name: input.name, value };
});
this.$emit('update-inputs', nameValuePairs);
},

View File

@ -24,15 +24,28 @@ const INPUT_TYPES = {
STRING: 'STRING',
};
const VALIDATION_MESSAGES = {
ARRAY_FORMAT_MISMATCH: __(
'The value must be a valid JSON array format: [1,2,3] or [{"key": "value"}]',
),
GENERAL_FORMAT_MISMATCH: __('Please match the requested format.'),
NUMBER_TYPE_MISMATCH: __('The value must contain only numbers.'),
REGEX_MISMATCH: __('The value must match the defined regular expression.'),
VALUE_MISSING: __('This is required and must be defined.'),
};
const feedbackMap = {
arrayFormatMismatch: {
isInvalid: (el) => {
if (el.dataset.jsonArray !== 'true' || !el.value) return false;
try {
const parsed = JSON.parse(el.value);
return !Array.isArray(parsed);
const isValid = Array.isArray(JSON.parse(el.value));
// we use setCustomValidity to set the message that appears when the user clicks submit
el.setCustomValidity(isValid ? '' : VALIDATION_MESSAGES.GENERAL_FORMAT_MISMATCH);
return !isValid;
} catch {
el.setCustomValidity(VALIDATION_MESSAGES.GENERAL_FORMAT_MISMATCH);
return true;
}
},
@ -40,21 +53,25 @@ const feedbackMap = {
},
numberTypeMismatch: {
isInvalid: (el) => {
return (
const isInvalid =
el.dataset.fieldType === INPUT_TYPES.NUMBER &&
el.value &&
!Number.isFinite(Number(el.value))
);
!Number.isFinite(Number(el.value));
// we use setCustomValidity to set the message that appears when the user clicks submit
el.setCustomValidity(isInvalid ? VALIDATION_MESSAGES.GENERAL_FORMAT_MISMATCH : '');
return isInvalid;
},
message: __('The value must contain only numbers.'),
message: VALIDATION_MESSAGES.NUMBER_TYPE_MISMATCH,
},
regexMismatch: {
isInvalid: (el) => el.validity?.patternMismatch,
message: __('The value must match the defined regular expression.'),
message: VALIDATION_MESSAGES.REGEX_MISMATCH,
},
valueMissing: {
isInvalid: (el) => el.validity?.valueMissing,
message: __('This is required and must be defined.'),
message: VALIDATION_MESSAGES.VALUE_MISSING,
},
};
@ -116,6 +133,13 @@ export default {
const field = this.form.fields[this.item.name];
return this.isArrayType && field?.feedback === feedbackMap.arrayFormatMismatch.message;
},
hasNumberTypeError() {
const field = this.form.fields[this.item.name];
return (
this.item.type === INPUT_TYPES.NUMBER &&
field?.feedback === feedbackMap.numberTypeMismatch.message
);
},
hasValidationFeedback() {
return Boolean(this.validationFeedback);
},
@ -137,9 +161,9 @@ export default {
: feedback;
},
validationState() {
// Override validation state for array format errors
// This handles cases where checkValidity() returns true but our custom array validation fails
if (this.hasArrayFormatError) {
// Override validation state for array format errors and number type errors for our custom validation
// This is also responsible for turning the border red when the input is invalid
if (this.hasArrayFormatError || this.hasNumberTypeError) {
return false;
}

View File

@ -2,7 +2,7 @@
import {
GlButton,
GlDisclosureDropdown,
GlDropdownDivider,
GlDisclosureDropdownGroup,
GlLoadingIcon,
GlTooltipDirective,
} from '@gitlab/ui';
@ -23,7 +23,7 @@ export default {
CiIcon,
GlButton,
GlDisclosureDropdown,
GlDropdownDivider,
GlDisclosureDropdownGroup,
GlLoadingIcon,
JobDropdownItem,
},
@ -161,22 +161,27 @@ export default {
data-testid="pipeline-mini-graph-dropdown-menu-list"
@click.stop
>
<span v-if="hasFailedJobs" class="gl-flex gl-px-4 gl-py-3 gl-text-sm gl-font-bold">
{{ s__('Pipelines|Failed jobs') }}
</span>
<job-dropdown-item
v-for="job in failedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
<gl-dropdown-divider v-if="hasPassedJobs && hasFailedJobs" />
<job-dropdown-item
v-for="job in passedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
<gl-disclosure-dropdown-group v-if="hasFailedJobs">
<template #group-label>{{ s__('Pipelines|Failed jobs') }}</template>
<job-dropdown-item
v-for="job in failedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
v-if="hasPassedJobs"
:bordered="hasFailedJobs"
data-testid="passed-jobs"
>
<job-dropdown-item
v-for="job in passedJobs"
:key="job.id"
:job="job"
@jobActionExecuted="$emit('jobActionExecuted')"
/>
</gl-disclosure-dropdown-group>
</ul>
<template #footer>

View File

@ -18,6 +18,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '~/ci/utils';
import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue';
import { fetchPolicies } from '~/lib/graphql';
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
import filterVariables from '../utils/filter_variables';
import {
CI_VARIABLE_TYPE_FILE,
@ -46,6 +47,7 @@ export default {
GlLoadingIcon,
GlSprintf,
InputsAdoptionBanner,
Markdown,
VariableValuesListbox,
},
mixins: [glFeatureFlagsMixin()],
@ -361,9 +363,11 @@ export default {
/>
</template>
</div>
<div v-if="descriptions[variable.key]" class="gl-text-subtle">
{{ descriptions[variable.key] }}
</div>
<markdown
v-if="descriptions[variable.key]"
class="gl-text-subtle"
:markdown="descriptions[variable.key]"
/>
</div>
<template #description>
<gl-sprintf

View File

@ -277,7 +277,7 @@ export default {
<template>
<div class="col-lg-8 gl-pl-0">
<gl-loading-icon v-if="loading && editing" size="lg" />
<gl-form v-else>
<gl-form v-else @submit.prevent="scheduleHandler">
<!--Description-->
<gl-form-group :label="$options.i18n.description" label-for="schedule-description">
<gl-form-input
@ -344,10 +344,10 @@ export default {
</gl-form-checkbox>
<div class="gl-flex gl-flex-wrap gl-gap-3">
<gl-button
type="submit"
variant="confirm"
data-testid="schedule-submit-button"
class="gl-w-full sm:gl-w-auto"
@click="scheduleHandler"
>
{{ buttonText }}
</gl-button>

View File

@ -1,10 +1,10 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapMutations } from 'vuex';
import { debounce } from 'lodash';
import { mapActions } from 'pinia';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import * as types from '~/diffs/store/mutation_types';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import {
INITIAL_TREE_WIDTH,
MIN_TREE_WIDTH,
@ -65,7 +65,7 @@ export default {
},
},
methods: {
...mapMutations('diffs', {
...mapActions(useLegacyDiffs, {
setCurrentDiffFile: types.SET_CURRENT_DIFF_FILE,
}),
onFileClick(file) {

View File

@ -6,13 +6,13 @@ import {
GlButton,
GlSearchBoxByType,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions, mapState } from 'pinia';
import micromatch from 'micromatch';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
import { isElementClipped } from '~/lib/utils/common_utils';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import DiffFileRow from './diff_file_row.vue';
import TreeListHeight from './tree_list_height.vue';
@ -48,8 +48,15 @@ export default {
};
},
computed: {
...mapState('diffs', ['renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds', 'realSize']),
...mapGetters('diffs', ['fileTree', 'allBlobs', 'linkedFile']),
...mapState(useLegacyDiffs, [
'renderTreeList',
'currentDiffFileId',
'viewedDiffFileIds',
'realSize',
'fileTree',
'allBlobs',
'linkedFile',
]),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@ -153,8 +160,7 @@ export default {
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'setRenderTreeList', 'setTreeOpen']),
...mapActions(useLegacyDiffs, ['toggleTreeOpen', 'setRenderTreeList', 'setTreeOpen']),
scrollVirtualScrollerToFileHash(hash) {
const item = document.querySelector(`[data-file-row="${hash}"]`);
if (item && !isElementClipped(item, this.$refs.scroller.$el)) return;

View File

@ -335,6 +335,7 @@ export default {
<template>
<div
class="detail-page-header-actions gl-mt-1 gl-flex gl-w-full gl-self-start sm:gl-gap-3 md:gl-w-auto"
data-testid="issue-header"
>
<div class="gl-w-full md:!gl-hidden">
<gl-disclosure-dropdown

View File

@ -592,11 +592,20 @@ export function insertMarkdownText({
}
}
export function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) {
export function updateText({
textArea,
tag,
cursorOffset,
blockTag,
wrap,
select,
tagContent,
replaceText = false,
}) {
const $textArea = $(textArea);
textArea = $textArea.get(0);
const text = $textArea.val();
const selected = selectedText(text, textArea) || tagContent;
const selected = replaceText ? '' : selectedText(text, textArea) || tagContent;
textArea.focus();
insertMarkdownText({
textArea,

View File

@ -18,6 +18,8 @@ import CommitInfo from './commit_info.vue';
import CollapsibleCommitInfo from './collapsible_commit_info.vue';
const trackingMixin = InternalEvents.mixin();
const POLL_INTERVAL = 30000;
export default {
components: {
CiIcon,
@ -49,7 +51,7 @@ export default {
};
},
update: (data) => {
const lastCommit = data.project?.repository?.paginatedTree?.nodes[0]?.lastCommit;
const lastCommit = data.project?.repository?.lastCommit ?? {};
const pipelines = lastCommit?.pipelines?.edges;
return {
@ -60,6 +62,7 @@ export default {
error(error) {
throw error;
},
pollInterval: POLL_INTERVAL,
subscribeToMore: {
document() {
return pipelineCiStatusUpdatedSubscription;

View File

@ -26,10 +26,13 @@ export const i18n = {
false,
),
improvementAndDegradationCopy: (improvement, degradation) =>
sprintf(__('Code Quality scans found %{degradation} and %{improvement}.'), {
improvement,
degradation,
}),
singularCopy: (findings) =>
sprintf(__('Code Quality scans found %{findings}.'), { findings }, false),
sprintf(
__('Code Quality scans found %{degradation} and %{improvement}.'),
{
improvement,
degradation,
},
false,
),
singularCopy: (findings) => sprintf(__('Code Quality scans found %{findings}.'), { findings }),
};

View File

@ -50,6 +50,7 @@ export default {
editorAiActions: { default: () => [] },
mrGeneratedContent: { default: null },
canSummarizeChanges: { default: false },
canUseComposer: { default: false },
},
props: {
previewMarkdown: {
@ -677,7 +678,7 @@ export default {
:new-comment-template-paths="commentTemplatePaths"
@select="insertSavedReply"
/>
<template v-if="!previewMarkdown && canSummarizeChanges">
<template v-if="!previewMarkdown && canSummarizeChanges && !canUseComposer">
<header-divider />
<summarize-code-changes />
</template>

View File

@ -1,6 +1,7 @@
<script>
import { GlAlert } from '@gitlab/ui';
import Autosize from 'autosize';
import MarkdownComposer from 'ee_component/vue_shared/components/markdown/composer.vue';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { updateDraft, clearDraft, getDraft } from '~/lib/utils/autosave';
@ -36,11 +37,13 @@ export default {
GlAlert,
MarkdownField,
LocalStorageSync,
MarkdownComposer,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
),
},
inject: { canUseComposer: { default: false } },
props: {
value: {
type: String,
@ -180,6 +183,9 @@ export default {
isDefaultEditorEnabled() {
return ['plain_text_editor', 'rich_text_editor'].includes(window.gon?.text_editor);
},
composerComponent() {
return this.canUseComposer ? 'markdown-composer' : 'div';
},
},
watch: {
value: 'updateValue',
@ -375,6 +381,7 @@ export default {
>
{{ alert.message }}
</gl-alert>
<!-- <markdown-composer v-if="!isContentEditorActive && canUseComposer" /> -->
<markdown-field
v-if="!isContentEditorActive"
ref="markdownField"
@ -402,20 +409,23 @@ export default {
<template #header-buttons><slot name="header-buttons"></slot></template>
<template #toolbar><slot name="toolbar"></slot></template>
<template #textarea>
<textarea
v-bind="formFieldProps"
ref="textarea"
:value="markdown"
class="note-textarea js-gfm-input markdown-area"
dir="auto"
:data-can-suggest="codeSuggestionsConfig.canSuggest"
:data-noteable-type="noteableType"
:data-supports-quick-actions="supportsQuickActions"
:data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
:disabled="disabled"
@input="updateMarkdownFromMarkdownField"
@keydown="$emit('keydown', $event)"
></textarea>
<component :is="composerComponent" :markdown="canUseComposer ? markdown : null">
<textarea
v-bind="formFieldProps"
ref="textarea"
:value="markdown"
class="note-textarea js-gfm-input markdown-area"
:class="[{ 'gl-relative gl-z-3 !gl-pl-7': canUseComposer }, formFieldProps.class || '']"
dir="auto"
:data-can-suggest="codeSuggestionsConfig.canSuggest"
:data-noteable-type="noteableType"
:data-supports-quick-actions="supportsQuickActions"
:data-testid="formFieldProps['data-testid'] || 'markdown-editor-form-field'"
:disabled="disabled"
@input="updateMarkdownFromMarkdownField"
@keydown="$emit('keydown', $event)"
></textarea>
</component>
</template>
</markdown-field>
<div v-else>

View File

@ -79,6 +79,7 @@ export function mountMarkdownEditor(options = {}) {
const supportsQuickActions = parseBoolean(el.dataset.supportsQuickActions ?? true);
const enableAutocomplete = parseBoolean(el.dataset.enableAutocomplete ?? true);
const disableAttachments = parseBoolean(el.dataset.disableAttachments ?? false);
const canUseComposer = parseBoolean(el.dataset.canUseComposer ?? false);
const autofocus = parseBoolean(el.dataset.autofocus ?? true);
const hiddenInput = el.querySelector('input[type="hidden"]');
const formFieldName = hiddenInput.getAttribute('name');
@ -98,6 +99,8 @@ export function mountMarkdownEditor(options = {}) {
componentConfiguration.apolloProvider =
options.apolloProvider || new VueApollo({ defaultClient: createApolloClient() });
componentConfiguration.provide.canUseComposer = canUseComposer;
// eslint-disable-next-line no-new
new Vue({
el,

View File

@ -2,7 +2,7 @@ import PromoPageLink from './promo_page_link.vue';
export default {
component: PromoPageLink,
title: 'vue_shared/help_page_link',
title: 'vue_shared/promo_page_link',
};
const Template = (args, { argTypes }) => ({

View File

@ -56,10 +56,6 @@ export default {
},
data() {
return {
expanded: false,
lines: [],
showLines: false,
loadingDiff: false,
isLoadingDescriptionVersion: false,
descriptionVersions: {},
};
@ -80,9 +76,6 @@ export default {
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
toggleIcon() {
return this.expanded ? 'chevron-up' : 'chevron-down';
},
actionTextHtml() {
return $(this.note.bodyHtml).unwrap().html();
},

View File

@ -20,7 +20,7 @@ export default {
type: String,
required: true,
},
sortFilterProp: {
sortFilter: {
type: String,
required: true,
},
@ -36,11 +36,7 @@ export default {
type: String,
required: true,
},
filterEvent: {
type: String,
required: true,
},
defaultSortFilterProp: {
defaultSortFilter: {
type: String,
required: true,
},
@ -50,6 +46,7 @@ export default {
},
},
computed: {
// eslint-disable-next-line vue/no-unused-properties
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@ -57,28 +54,20 @@ export default {
property: `type_${this.workItemType}`,
};
},
getDropdownSelectedText() {
return this.selectedSortOption.text;
dropdownText() {
return this.selectedItem.text;
},
selectedSortOption() {
return (
this.items.find(({ key }) => this.sortFilterProp === key) || this.defaultSortFilterProp
);
selectedItem() {
return this.items.find(({ key }) => this.sortFilter === key) || this.defaultSortFilter;
},
},
methods: {
setDiscussionFilterOption(filterValue) {
this.$emit(this.filterEvent, filterValue);
},
fetchFilteredDiscussions(filterValue) {
if (this.isSortDropdownItemActive(filterValue)) {
handleSelect(sortFilter) {
if (sortFilter === this.sortFilter) {
return;
}
this.track(this.trackingAction);
this.$emit(this.filterEvent, filterValue);
},
isSortDropdownItemActive(value) {
return value === this.sortFilterProp;
this.$emit('select', sortFilter);
},
},
};
@ -87,18 +76,19 @@ export default {
<template>
<div class="gl-inline-block gl-align-bottom">
<local-storage-sync
:value="sortFilterProp"
:value="sortFilter"
:storage-key="storageKey"
as-string
@input="setDiscussionFilterOption"
@input="$emit('select', $event)"
/>
<gl-collapsible-listbox
:toggle-text="getDropdownSelectedText"
:disabled="loading"
:toggle-text="dropdownText"
:items="items"
:selected="sortFilterProp"
:selected="sortFilter"
placement="bottom-end"
size="small"
@select="fetchFilteredDiscussions"
@select="handleSelect"
/>
</div>
</template>

View File

@ -4,7 +4,6 @@ import { uniqueId } from 'lodash';
import { visitUrl } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import { findWidget } from '~/issues/list/utils';
@ -64,11 +63,6 @@ export default {
type: String,
required: true,
},
sortOrder: {
type: String,
required: false,
default: ASC,
},
markdownPreviewPath: {
type: String,
required: true,
@ -147,7 +141,6 @@ export default {
workItem: {},
isEditing: this.isNewDiscussion,
isSubmitting: false,
isSubmittingWithKeydown: false,
};
},
apollo: {
@ -181,6 +174,7 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
return this.discussionId ? `${this.discussionId}-comment` : `${this.workItemId}-comment`;
},
// eslint-disable-next-line vue/no-unused-properties
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,

View File

@ -32,9 +32,6 @@ export default {
projectArchivedWarning: __('This project is archived and cannot be commented on.'),
},
computed: {
issuableDisplayName() {
return this.workItemType.replace(/_/g, ' ');
},
lockedIssueWarning() {
return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
noteableTypeText: this.noteableTypeText,

View File

@ -130,6 +130,7 @@ export default {
};
},
computed: {
// eslint-disable-next-line vue/no-unused-properties
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@ -389,7 +390,6 @@ export default {
:note-url="noteUrl"
:show-reply="showReply"
:show-edit="hasAdminPermission"
:note-id="note.id"
:is-author-an-assignee="isAuthorAnAssignee"
:show-assign-unassign="canSetWorkItemMetadata && hasAuthor"
:can-report-abuse="!isCurrentUserAuthorOfNote"
@ -399,7 +399,6 @@ export default {
:max-access-level-of-author="note.maxAccessLevelOfAuthor"
:project-name="projectName"
:can-resolve="canResolve"
:resolvable="isDiscussionResolvable"
:is-resolved="isDiscussionResolved"
:is-resolving="isResolving"
:resolved-by="discussionResolvedBy"
@ -444,7 +443,6 @@ export default {
ref="noteBody"
:note="note"
:has-admin-note-permission="hasAdminPermission"
:has-replies="hasReplies"
:is-updating="isUpdating"
@updateNote="updateNote"
/>
@ -463,7 +461,6 @@ export default {
:full-path="fullPath"
:note="note"
:work-item-iid="workItemIid"
:is-modal="isModal"
/>
</div>
</div>

View File

@ -57,10 +57,6 @@ export default {
type: Boolean,
required: true,
},
noteId: {
type: String,
required: true,
},
showAwardEmoji: {
type: Boolean,
required: false,
@ -115,11 +111,6 @@ export default {
required: false,
default: false,
},
resolvable: {
type: Boolean,
required: false,
default: false,
},
isResolved: {
type: Boolean,
required: false,

View File

@ -21,11 +21,6 @@ export default {
type: Object,
required: true,
},
isModal: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
awards() {

View File

@ -20,11 +20,6 @@ export default {
required: false,
default: false,
},
hasReplies: {
type: Boolean,
required: false,
default: false,
},
isUpdating: {
type: Boolean,
required: false,

View File

@ -59,14 +59,6 @@ export default {
return this.smallHeaderStyle ? 'gl-text-base gl-m-0' : 'gl-text-size-h1 gl-m-0';
},
},
methods: {
changeNotesSortOrder(direction) {
this.$emit('changeSort', direction);
},
filterDiscussions(filterValue) {
this.$emit('changeFilter', filterValue);
},
},
WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
WORK_ITEM_NOTES_FILTER_KEY,
WORK_ITEM_NOTES_FILTER_ALL_NOTES,
@ -93,28 +85,26 @@ export default {
<work-item-activity-sort-filter
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="discussionFilter"
:sort-filter="discussionFilter"
:items="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
:default-sort-filter-prop="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
:default-sort-filter="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
tracking-action="work_item_notes_filter_changed"
tracking-label="item_track_notes_filtering"
filter-event="changeFilter"
data-testid="work-item-filter"
@changeFilter="filterDiscussions"
@select="$emit('changeFilter', $event)"
/>
<work-item-activity-sort-filter
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="sortOrder"
:sort-filter="sortOrder"
:items="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
:default-sort-filter-prop="$options.ASC"
:default-sort-filter="$options.ASC"
tracking-action="work_item_notes_sort_order_changed"
tracking-label="item_track_notes_sorting"
filter-event="changeSort"
data-testid="work-item-sort"
@changeSort="changeNotesSortOrder"
@select="$emit('changeSort', $event)"
/>
</div>
</div>

View File

@ -139,13 +139,7 @@ export default {
WorkItemCreateBranchMergeRequestSplitButton,
},
mixins: [glFeatureFlagMixin(), trackingMixin],
inject: [
'fullPath',
'reportAbusePath',
'groupPath',
'hasSubepicsFeature',
'hasLinkedItemsEpicsFeature',
],
inject: ['fullPath', 'groupPath', 'hasSubepicsFeature', 'hasLinkedItemsEpicsFeature'],
props: {
isModal: {
type: Boolean,
@ -1235,7 +1229,6 @@ export default {
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
:can-set-work-item-metadata="canAssignUnassignUser"
:can-summarize-comments="canSummarizeComments"
:report-abuse-path="reportAbusePath"
:is-discussion-locked="isDiscussionLocked"
:is-work-item-confidential="workItem.confidential"
:new-comment-template-paths="newCommentTemplatePaths"

View File

@ -92,10 +92,6 @@ export default {
required: false,
default: false,
},
reportAbusePath: {
type: String,
required: true,
},
isDiscussionLocked: {
type: Boolean,
required: false,
@ -153,9 +149,6 @@ export default {
someNotesLoaded() {
return !this.initialLoading || this.previewNote;
},
avatarUrl() {
return window.gon.current_user_avatar_url;
},
pageInfo() {
return this.workItemNotes?.pageInfo;
},
@ -379,10 +372,10 @@ export default {
isSystemNote(note) {
return note.notes.nodes[0].system;
},
changeNotesSortOrder(direction) {
setSort(direction) {
this.sortOrder = direction;
},
filterDiscussions(filterValue) {
setFilter(filterValue) {
this.discussionFilter = filterValue;
},
reportAbuse(isOpen, reply = {}) {
@ -469,8 +462,8 @@ export default {
:discussion-filter="discussionFilter"
:use-h2="useH2"
:small-header-style="smallHeaderStyle"
@changeSort="changeNotesSortOrder"
@changeFilter="filterDiscussions"
@changeSort="setSort"
@changeFilter="setFilter"
/>
<work-item-notes-loading v-if="initialLoading" class="gl-mt-5" />
<div v-if="someNotesLoaded" class="issuable-discussion gl-mb-5 !gl-clearfix">
@ -522,10 +515,7 @@ export default {
</template>
</template>
<work-item-history-only-filter-note
v-if="commentsDisabled"
@changeFilter="filterDiscussions"
/>
<work-item-history-only-filter-note v-if="commentsDisabled" @changeFilter="setFilter" />
</ul>
<work-item-notes-loading v-if="!formAtTop && isLoadingMore" />
<div v-if="!formAtTop && !commentsDisabled" class="js-comment-form">

View File

@ -60,11 +60,13 @@ module Groups
end
def sort(items)
return super unless params[:sort]
if params[:sort] == :similarity && params[:search].present?
return items.sorted_by_similarity_desc(params[:search])
end
super
items.sort_by_attribute(params[:sort])
end
end
end

View File

@ -4,68 +4,62 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!, $refType:
id
repository {
__typename
paginatedTree(path: $path, ref: $ref, refType: $refType) {
lastCommit(path: $path, ref: $ref, refType: $refType) {
__typename
nodes {
id
sha
title
titleHtml
descriptionHtml
message
webPath
authoredDate
authorName
authorGravatar
author {
__typename
lastCommit {
id
name
avatarUrl
webPath
}
signature {
__typename
... on GpgSignature {
gpgKeyPrimaryKeyid
verificationStatus
}
... on X509Signature {
verificationStatus
x509Certificate {
id
subject
subjectKeyIdentifier
x509Issuer {
id
subject
subjectKeyIdentifier
}
}
}
... on SshSignature {
verificationStatus
keyFingerprintSha256
}
}
pipelines(ref: $ref, first: 1) {
__typename
edges {
__typename
id
sha
title
titleHtml
descriptionHtml
message
webPath
authoredDate
authorName
authorGravatar
author {
node {
__typename
id
name
avatarUrl
webPath
}
signature {
__typename
... on GpgSignature {
gpgKeyPrimaryKeyid
verificationStatus
}
... on X509Signature {
verificationStatus
x509Certificate {
id
subject
subjectKeyIdentifier
x509Issuer {
id
subject
subjectKeyIdentifier
}
}
}
... on SshSignature {
verificationStatus
keyFingerprintSha256
}
}
pipelines(ref: $ref, first: 1) {
__typename
edges {
detailedStatus {
__typename
node {
__typename
id
detailedStatus {
__typename
id
detailsPath
icon
text
}
}
id
detailsPath
icon
text
}
}
}

View File

@ -25,11 +25,21 @@ module Resolvers
required: false,
description: 'Filter catalog resources by verification level.'
def resolve_with_lookahead(scope:, search: nil, sort: nil, verification_level: nil)
argument :topics, [GraphQL::Types::String],
required: false,
description: 'Filter catalog resources by project topic names.'
def resolve_with_lookahead(scope:, search: nil, sort: nil, verification_level: nil, topics: nil)
apply_lookahead(
::Ci::Catalog::Listing
.new(context[:current_user])
.resources(sort: sort, search: search, scope: scope, verification_level: verification_level)
.resources(
sort: sort,
search: search,
scope: scope,
verification_level: verification_level,
topics: topics
)
)
end

View File

@ -26,19 +26,21 @@ module Resolvers
GitlabSchema.parse_gids(global_ids, expected_type: ::Group).map(&:model_id)
}
argument :sort, Types::Namespaces::GroupSortEnum,
required: false,
description: 'Sort groups by given criteria.',
default_value: :name_asc
alias_method :parent, :object
private
# rubocop: disable CodeReuse/ActiveRecord
def resolve_groups(args)
return Group.none unless parent.present?
GroupsFinder
.new(context[:current_user], args.merge(parent: parent))
.execute
.reorder(name: :asc)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end

View File

@ -9,6 +9,27 @@ module Types
value 'SIMILARITY',
'Most similar to the search query.',
value: :similarity
value 'NAME_ASC',
'Sort by name, ascending order.',
value: :name_asc
value 'NAME_DESC',
'Sort by name, descending order.',
value: :name_desc
value 'PATH_ASC',
'Sort by path, ascending order.',
value: :path_asc
value 'PATH_DESC',
'Sort by path, descending order.',
value: :path_desc
value 'ID_ASC',
'Sort by ID, ascending order.',
value: :id_asc
value 'ID_DESC',
'Sort by ID, descending order.',
value: :id_desc
end
end
end

View File

@ -184,6 +184,10 @@ module MergeRequestsHelper
Feature.enabled?(:notifications_todos_buttons, current_user)
end
def can_use_description_composer(_user, _merge_request)
false
end
def diffs_tab_pane_data(project, merge_request, params)
{
"is-locked": merge_request.discussion_locked?,

View File

@ -10,11 +10,12 @@ module Ci
@current_user = current_user
end
def resources(sort: nil, search: nil, scope: :all, verification_level: nil)
def resources(sort: nil, search: nil, scope: :all, verification_level: nil, topics: nil)
relation = Ci::Catalog::Resource.published.includes(:project)
relation = by_scope(relation, scope)
relation = by_search(relation, search)
relation = by_verification_level(relation, verification_level)
relation = by_topics(relation, topics)
case sort.to_s
when 'name_desc' then relation.order_by_name_desc
@ -65,6 +66,12 @@ module Ci
relation.for_verification_level(level)
end
def by_topics(relation, topics)
return relation if topics.blank?
relation.with_topics(topics)
end
end
end
end

View File

@ -52,6 +52,15 @@ module Ci
)
end
scope :with_topics, ->(topic_names) do
joins(:project)
.where(project_id: Projects::ProjectTopic
.joins(:topic)
.where(topics: { name: topic_names })
.select(:project_id))
.distinct
end
# The usage counts are updated daily by Ci::Catalog::Resources::AggregateLast30DayUsageWorker
scope :order_by_last_30_day_usage_count_desc, -> { reorder(last_30_day_usage_count: :desc) }
scope :order_by_last_30_day_usage_count_asc, -> { reorder(last_30_day_usage_count: :asc) }

View File

@ -8,9 +8,10 @@ module Groups # rubocop:disable Gitlab/BoundedContexts -- existing top-level mod
return error(_('Cannot mark group for deletion: feature not supported')) unless licensed || feature_downtiered?
result = create_deletion_schedule
log_event if result[:status] == :success
send_group_deletion_notification
if result[:status] == :success
log_event
send_group_deletion_notification
end
result
end

View File

@ -28,6 +28,7 @@
autofocus: 'false',
form_field_classes: 'js-gfm-input markdown-area note-textarea rspec-issuable-form-description',
project_id: @project.id,
can_use_composer: is_merge_request ? can_use_description_composer(current_user, model).to_s : nil,
source_branch: is_merge_request ? @merge_request.source_branch : nil,
target_branch: is_merge_request ? @merge_request.target_branch : nil,
can_summarize: is_merge_request ? can?(current_user, :access_summarize_new_merge_request, @project).to_s : nil } }

View File

@ -3,7 +3,7 @@
%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { testid: 'issuable-form-title-field' }
autocomplete: 'off', class: 'form-control pad js-issuable-title', dir: 'auto', data: { testid: 'issuable-form-title-field' }
- if issuable.respond_to?(:draft?)
.gl-pt-3

View File

@ -0,0 +1,8 @@
---
migration_job_name: DeleteOrphanedRoutes
description: Deletes the orphaned routes that were not deleted by the loose foreign key
feature_category: groups_and_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186659
milestone: '17.11'
queued_migration_version: 20250401113424
finalized_by: # version of the migration that finalized this BBM

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddSnoozedUntilToSecurityPipelineExecutionProjectSchedules < Gitlab::Database::Migration[2.2]
milestone '17.11'
def change
add_column :security_pipeline_execution_project_schedules, :snoozed_until, :datetime_with_timezone
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class QueueDeleteOrphanedRoutes < Gitlab::Database::Migration[2.2]
milestone '17.11'
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
MIGRATION = "DeleteOrphanedRoutes"
DELAY_INTERVAL = 2.minutes
def up
queue_batched_background_migration(
MIGRATION,
:routes,
:id,
job_interval: DELAY_INTERVAL
)
end
def down
delete_batched_background_migration(MIGRATION, :routes, :id, [])
end
end

View File

@ -0,0 +1 @@
1ebfd67fe8514f3b06285f4f76a8e7a6352515fd72ac1dffc5a7db77235ad7e4

View File

@ -0,0 +1 @@
ed8f85ba760246615dd19e270ac09c0dc1b381d24dee2143dcc8296ed9bc8c71

View File

@ -22547,6 +22547,7 @@ CREATE TABLE security_pipeline_execution_project_schedules (
time_window_seconds integer NOT NULL,
cron text NOT NULL,
cron_timezone text NOT NULL,
snoozed_until timestamp with time zone,
CONSTRAINT check_b93315bfbb CHECK ((char_length(cron_timezone) <= 255)),
CONSTRAINT check_bbbe4b1b8d CHECK ((char_length(cron) <= 128)),
CONSTRAINT check_c440017377 CHECK ((time_window_seconds > 0))

View File

@ -390,6 +390,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="querycicatalogresourcesscope"></a>`scope` | [`CiCatalogResourceScope`](#cicatalogresourcescope) | Scope of the returned catalog resources. |
| <a id="querycicatalogresourcessearch"></a>`search` | [`String`](#string) | Search term to filter the catalog resources by name or description. |
| <a id="querycicatalogresourcessort"></a>`sort` | [`CiCatalogResourceSort`](#cicatalogresourcesort) | Sort catalog resources by given criteria. |
| <a id="querycicatalogresourcestopics"></a>`topics` | [`[String!]`](#string) | Filter catalog resources by project topic names. |
| <a id="querycicatalogresourcesverificationlevel"></a>`verificationLevel` | [`CiCatalogResourceVerificationLevel`](#cicatalogresourceverificationlevel) | Filter catalog resources by verification level. |
### `Query.ciConfig`
@ -27569,6 +27570,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="groupdescendantgroupsincludeparentdescendants"></a>`includeParentDescendants` | [`Boolean`](#boolean) | List of descendant groups of the parent group. |
| <a id="groupdescendantgroupsowned"></a>`owned` | [`Boolean`](#boolean) | Limit result to groups owned by authenticated user. |
| <a id="groupdescendantgroupssearch"></a>`search` | [`String`](#string) | Search query for group name or group full path. |
| <a id="groupdescendantgroupssort"></a>`sort` | [`GroupSort`](#groupsort) | Sort groups by given criteria. |
##### `Group.doraPerformanceScoreCounts`
@ -43006,6 +43008,12 @@ Values for sorting groups.
| Value | Description |
| ----- | ----------- |
| <a id="groupsortid_asc"></a>`ID_ASC` | Sort by ID, ascending order. |
| <a id="groupsortid_desc"></a>`ID_DESC` | Sort by ID, descending order. |
| <a id="groupsortname_asc"></a>`NAME_ASC` | Sort by name, ascending order. |
| <a id="groupsortname_desc"></a>`NAME_DESC` | Sort by name, descending order. |
| <a id="groupsortpath_asc"></a>`PATH_ASC` | Sort by path, ascending order. |
| <a id="groupsortpath_desc"></a>`PATH_DESC` | Sort by path, descending order. |
| <a id="groupsortsimilarity"></a>`SIMILARITY` | Most similar to the search query. |
### `GroupingEnum`

View File

@ -147,12 +147,6 @@ Fallback keys follow the same processing logic as `cache:key`:
### Global fallback key
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/1534) in GitLab Runner 13.4.
{{< /history >}}
You can use the `$CI_COMMIT_REF_SLUG` [predefined variable](../variables/predefined_variables.md)
to specify your [`cache:key`](../yaml/_index.md#cachekey). For example, if your
`$CI_COMMIT_REF_SLUG` is `test`, you can set a job to download cache that's tagged with `test`.

View File

@ -106,8 +106,6 @@ to include the file.
[[runners.kubernetes.volumes.config_map]]
name = "docker-client-config"
mount_path = "/root/.docker/config.json"
# If you are running GitLab Runner 13.5
# or lower you can remove this
sub_path = "config.json"
```

View File

@ -277,11 +277,6 @@ variables:
# The 'docker' hostname is the alias of the service container as described at
# https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services
#
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
# the variable must be set to tcp://localhost:2375 because of how the
# Kubernetes executor connects services to the job container
# DOCKER_HOST: tcp://localhost:2375
#
DOCKER_HOST: tcp://docker:2375
#
# This instructs Docker not to start over TLS.
@ -348,10 +343,6 @@ To use Docker-in-Docker with TLS enabled in Kubernetes:
#
# The 'docker' hostname is the alias of the service container as described at
# https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
# the variable must be set to tcp://localhost:2376 because of how the
# Kubernetes executor connects services to the job container
# DOCKER_HOST: tcp://localhost:2376
#
# Specify to Docker where to create the certificates. Docker
# creates them automatically on boot, and creates
@ -416,10 +407,6 @@ For example:
#
# The 'docker' hostname is the alias of the service container as described at
# https://docs.gitlab.com/ee/ci/services/#accessing-the-services.
# If you're using GitLab Runner 12.7 or earlier with the Kubernetes executor and Kubernetes 1.6 or earlier,
# the variable must be set to tcp://localhost:2376 because of how the
# Kubernetes executor connects services to the job container
# DOCKER_HOST: tcp://localhost:2376
#
# This instructs Docker not to start over TLS.
DOCKER_TLS_CERTDIR: ""

View File

@ -249,10 +249,6 @@ To define which option should be used, the runner process reads the configuratio
### Requirements and limitations
- Available for [Docker executor](https://docs.gitlab.com/runner/executors/docker/)
in GitLab Runner 12.0 and later.
- Available for [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes/)
in GitLab Runner 13.1 and later.
- [Credentials Store](#use-a-credentials-store) and [Credential Helpers](#use-credential-helpers)
require binaries to be added to the GitLab Runner `$PATH`, and require access to do so. Therefore,
these features are not available on instance runners, or any other runner where the user does not
@ -260,13 +256,6 @@ To define which option should be used, the runner process reads the configuratio
### Use statically-defined credentials
{{< history >}}
- Introduced in GitLab Runner 1.8 for Docker executor.
- Introduced in GitLab Runner 13.1 for Kubernetes executor.
{{< /history >}}
You can access a private registry using two approaches. Both require setting the CI/CD variable
`DOCKER_AUTH_CONFIG` with appropriate authentication information.
@ -411,13 +400,6 @@ To add `DOCKER_AUTH_CONFIG` to a runner:
### Use a Credentials Store
{{< history >}}
- Introduced in GitLab Runner 9.5 for Docker executor.
- Introduced in GitLab Runner 13.1 for Kubernetes executor.
{{< /history >}}
To configure a Credentials Store:
1. To use a Credentials Store, you need an external helper program to interact with a specific keychain or external store.
@ -446,13 +428,6 @@ pulling from Docker Hub fails. Docker daemon tries to use the same credentials f
### Use Credential Helpers
{{< history >}}
- Introduced in GitLab Runner 12.0 for Docker executor.
- Introduced in GitLab Runner 13.1 for Kubernetes executor.
{{< /history >}}
As an example, let's assume that you want to use the `<aws_account_id>.dkr.ecr.<region>.amazonaws.com/private/image:latest`
image. This image is private and requires you to sign in to a private container registry.

View File

@ -258,19 +258,6 @@ a `service`.
This functionality is covered in [the CI services](../services/_index.md)
documentation.
## Testing things locally
With GitLab Runner 1.0 you can also test any changes locally. From your
terminal execute:
```shell
# Check using docker executor
gitlab-runner exec docker test:app
# Check using shell executor
gitlab-runner exec shell test:app
```
## Example project
We have set up an [Example PHP Project](https://gitlab.com/gitlab-examples/php) for your convenience

View File

@ -104,10 +104,17 @@ The pipeline now executes the jobs as configured.
#### Prefill variables in manual pipelines
{{< history >}}
- Markdown rendering on the **Run pipeline** page [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/441474) in GitLab 17.11.
{{< /history >}}
You can use the [`description` and `value`](../yaml/_index.md#variablesdescription)
keywords to [define pipeline-level (global) variables](../variables/_index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
that are prefilled when running a pipeline manually. Use the description to explain
information such as what the variable is used for, and what the acceptable values are.
You can use Markdown in the description.
Job-level variables cannot be pre-filled.

View File

@ -691,12 +691,6 @@ Where `$REFSPECS` is a value provided to the runner internally by GitLab.
### Sync or exclude specific submodules from CI jobs
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/2249) in GitLab Runner 14.0.
{{< /history >}}
Use the `GIT_SUBMODULE_PATHS` variable to control which submodules have to be synced or updated.
You can set it globally or per-job in the [`variables`](../yaml/_index.md#variables) section.

View File

@ -269,34 +269,16 @@ test:
## Available settings for `services`
{{< history >}}
- Introduced in GitLab and GitLab Runner 9.4.
{{< /history >}}
| Setting | Required | GitLab version | Description |
|-----------------------------------|--------------------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | yes, when used with any other option | 9.4 | Full name of the image to use. If the full image name includes a registry hostname, use the `alias` option to define a shorter service access name. For more information, see [Accessing the services](#accessing-the-services). |
| `entrypoint` | no | 9.4 | Command or script to execute as the container's entrypoint. It's translated to the Docker `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint) directive, where each shell token is a separate string in the array. |
| `command` | no | 9.4 | Command or script that should be used as the container's command. It's translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`](https://docs.docker.com/reference/dockerfile/#cmd) directive, where each shell token is a separate string in the array. |
| `alias` <sup>1</sup> <sup>3</sup> | no | 9.4 | Additional aliases to access the service from the job's container. Multiple aliases can be separated by spaces or commas. For more information, see [Accessing the services](#accessing-the-services). |
| `variables` <sup>2</sup> | no | 14.5 | Additional environment variables that are passed exclusively to the service. The syntax is the same as [Job Variables](../variables/_index.md). Service variables cannot reference themselves. |
**Footnotes:**
1. Alias support for the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2229) in GitLab Runner 12.8, and is only available for Kubernetes version 1.7 or later.
1. Service variables support for the Docker and the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3158) in GitLab Runner 14.8.
1. Use alias as a container name for the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421131) in GitLab Runner 17.9. For more information, see [Configuring the service containers name with the Kubernetes executor](#using-aliases-as-service-container-names-for-the-kubernetes-executor).
| Setting | Required | GitLab version | Description |
|--------------|--------------------------------------|----------------|-------------|
| `name` | yes, when used with any other option | 9.4 | Full name of the image to use. If the full image name includes a registry hostname, use the `alias` option to define a shorter service access name. For more information, see [Accessing the services](#accessing-the-services). |
| `entrypoint` | no | 9.4 | Command or script to execute as the container's entrypoint. It's translated to the Docker `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint) directive, where each shell token is a separate string in the array. |
| `command` | no | 9.4 | Command or script that should be used as the container's command. It's translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`](https://docs.docker.com/reference/dockerfile/#cmd) directive, where each shell token is a separate string in the array. |
| `alias` | no | 9.4 | Additional aliases to access the service from the job's container. Multiple aliases can be separated by spaces or commas. For more information, see [Accessing the services](#accessing-the-services). Using alias as a container name for the Kubernetes executor was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421131) in GitLab Runner 17.9. For more information, see [Configuring the service containers name with the Kubernetes executor](#using-aliases-as-service-container-names-for-the-kubernetes-executor). |
| `variables` | no | 14.5 | Additional environment variables that are passed exclusively to the service. The syntax is the same as [Job Variables](../variables/_index.md). Service variables cannot reference themselves. |
## Starting multiple services from the same image
{{< history >}}
- Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended configuration options](../docker/using_docker_images.md#extended-docker-configuration-options).
{{< /history >}}
Before the new extended Docker configuration options, the following configuration
would not work properly:
@ -328,12 +310,6 @@ in `.gitlab-ci.yml` file.
## Setting a command for the service
{{< history >}}
- Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended configuration options](../docker/using_docker_images.md#extended-docker-configuration-options).
{{< /history >}}
Let's assume you have a `super/sql:latest` image with some SQL database
in it. You would like to use it as a service for your job. Let's also
assume that this image does not start the database process while starting

View File

@ -78,9 +78,6 @@ java:
junit: build/test-results/test/**/TEST-*.xml
```
In [GitLab Runner 13.0](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620)
and later, you can use `**`.
### Maven
For parsing [Surefire](https://maven.apache.org/surefire/maven-surefire-plugin/)

View File

@ -1344,11 +1344,7 @@ link outside it.
**Supported values**:
- An array of file paths, relative to the project directory.
- You can use Wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming))
patterns and:
- In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620),
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
- In GitLab Runner 12.10 and earlier, [`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
- You can use Wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns and [`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match) patterns.
- For [GitLab Pages job](#pages):
- In [GitLab 17.10 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/428018),
the [`pages.publish`](#pagespublish) path is automatically appended to `artifacts:paths`,
@ -1787,12 +1783,8 @@ Use the `cache:paths` keyword to choose which files or directories to cache.
**Supported values**:
- An array of paths relative to the project directory (`$CI_PROJECT_DIR`).
You can use wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming))
patterns:
- In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620),
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
- In GitLab Runner 12.10 and earlier,
[`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
You can use wildcards that use [glob](https://en.wikipedia.org/wiki/Glob_(programming)) and
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match) patterns.
[CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file) are supported.
@ -6113,7 +6105,7 @@ The description displays with [the prefilled variable name when running a pipeli
**Supported values**:
- A string.
- A string. You can use Markdown.
**Example of `variables:description`**:

View File

@ -186,6 +186,34 @@ The [subscription for Chat](duo_chat.md#graphql-subscription) behaves differentl
To not have many concurrent subscriptions, you should also only subscribe to the subscription once the mutation is sent by using [`skip()`](https://apollo.vuejs.org/guide-option/subscriptions.html#skipping-the-subscription).
##### Clarifying different ID parameters
When working with the `aiAction` mutation, several ID parameters are used for routing requests and responses correctly. Here's what each parameter does:
- **user_id** (required)
- Purpose: Identifies and authenticates the requesting user
- Used for: Permission checks, request attribution, and response routing
- Example: `gid://gitlab/User/123`
- Note: This ID is automatically included by the GraphQL API framework
- **client_subscription_id** (recommended for streaming or multiple features)
- Client-generated UUID for tracking specific request/response pairs
- Required when using streaming responses or when multiple AI features share the same page
- Example: `"9f5dedb3-c58d-46e3-8197-73d653c71e69"`
- Can be omitted for simple, isolated requests with no streaming
- **resource_id** (contextual - required for some features)
- Purpose: References a specific GitLab entity (project, issue, MR) that provides context for the AI operation
- Used for: Permission verification and contextual information gathering
- Real example: `"gid://gitlab/Issue/164723626"`
- Note: Some features may not require a specific resource
- **project_id** (contextual - required for some features)
- Purpose: Identifies the project context for the AI operation
- Used for: Project-specific permission checks and context
- Real example: `"gid://gitlab/Project/278964"`
- Note: Some features may not require a specific project
#### Current abstraction layer flow
The following graph uses VertexAI as an example. You can use different providers.

View File

@ -29,6 +29,8 @@ query: label = "AI Model Migration" AND opened = true
LLM models are constantly evolving, and GitLab needs to regularly update our AI features to support newer models. This guide provides a structured approach for migrating AI features to new models while maintaining stability and reliability.
*Note: GitLab strives to leverage the latest AI model capabilities to help provide optimal performance and features, which means model updates from existing GitLab subprocessors might occur without specific customer notifications beyond documentation updates.*
## Model Migration Timelines
Model migrations typically follow these general timelines:

View File

@ -20,10 +20,7 @@ This page contains possible solutions for problems you might encounter when usin
## SAML debugging tools
SAML responses are base64 encoded, so we recommend the following browser plugins to decode them on the fly:
- [SAML-tracer](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/) for Firefox.
- [SAML Message Decoder](https://chromewebstore.google.com/detail/mpabchoaimgbdbbjjieoaeiibojelbhm?hl=en) for Chrome.
SAML responses are base64 encoded. To decode them on the fly you can use the **SAML-tracer** browser extension ([Firefox](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/), [Chrome](https://chromewebstore.google.com/detail/saml-tracer/mpdajninpobndbfcldcmbpnnbhibjmch?hl=en)).
If you cannot install a browser plugin, you can [manually generate and capture a SAML response](#manually-generate-a-saml-response) instead.

View File

@ -85,12 +85,40 @@ module API
end
end
def immediately_delete_project_error(project)
if !project.marked_for_deletion_at?
'Project must be marked for deletion first.'
elsif project.full_path != params[:full_path]
'`full_path` is incorrect. You must enter the complete path for the project.'
end
end
def delete_project(user_project)
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
permanently_remove = ::Gitlab::Utils.to_boolean(params[:permanently_remove])
if permanently_remove && user_project.adjourned_deletion_configured?
error = immediately_delete_project_error(user_project)
return render_api_error!(error, 400) if error
end
accepted!
if permanently_remove || !user_project.adjourned_deletion_configured?
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
return accepted!
end
result = destroy_conditionally!(user_project) do
::Projects::MarkForDeletionService.new(user_project, current_user, {}).execute
end
if result[:status] == :success
accepted!
else
render_api_error!(result[:message], 400)
end
end
def validate_projects_api_rate_limit_for_unauthenticated_users!

View File

@ -237,11 +237,15 @@ module Gitlab
end
def save_current_token_in_env
::Current.token_info = {
token_info = {
token_id: access_token.id,
token_type: access_token.class.to_s,
token_scopes: access_token.scopes.map(&:to_sym)
}
token_info[:token_application_id] = access_token.application_id if access_token.respond_to?(:application_id)
::Current.token_info = token_info
end
def save_auth_failure_in_application_context(access_token, cause, requested_scopes)

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class DeleteOrphanedRoutes < BatchedMigrationJob
operation_name :delete_orphaned_routes
feature_category :groups_and_projects
def perform
each_sub_batch do |sub_batch|
sub_batch
.joins('LEFT JOIN namespaces ON namespaces.id = routes.namespace_id')
.where(namespaces: { id: nil })
.delete_all
end
end
end
end
end

View File

@ -9,7 +9,7 @@ module Gitlab
return {} unless params
params.slice(:token_type, :token_id)
params.slice(:token_type, :token_id, :token_application_id)
end
end
end

View File

@ -1055,6 +1055,9 @@ msgstr ""
msgid "%{labelStart}Project:%{labelEnd} %{project}"
msgstr ""
msgid "%{labelStart}Reachable:%{labelEnd} %{reachability}"
msgstr ""
msgid "%{labelStart}Report type:%{labelEnd} %{reportType}"
msgstr ""
@ -6388,9 +6391,6 @@ msgstr ""
msgid "AmazonQ|Audience"
msgstr ""
msgid "AmazonQ|Automatic code reviews can only be enabled when Amazon Q is set to \"On by default\""
msgstr ""
msgid "AmazonQ|Availability"
msgstr ""
@ -6430,9 +6430,6 @@ msgstr ""
msgid "AmazonQ|Delete the IAM role."
msgstr ""
msgid "AmazonQ|Enable automatic code reviews"
msgstr ""
msgid "AmazonQ|Enter the IAM role's ARN."
msgstr ""
@ -6457,6 +6454,9 @@ msgstr ""
msgid "AmazonQ|GitLab Duo with Amazon Q is ready to go! 🎉"
msgstr ""
msgid "AmazonQ|Have Amazon Q review code in merge requests automatically"
msgstr ""
msgid "AmazonQ|I understand that by selecting Save changes, GitLab creates a service account for Amazon Q and sends its credentials to AWS. Use of the Amazon Q Developer capabilities as part of GitLab Duo with Amazon Q is governed by the %{helpStart}AWS Customer Agreement%{helpEnd} or other written agreement between you and AWS governing your use of AWS services. Amazon Q Developer processes data across all US Regions and makes cross-region API calls when your requests require it."
msgstr ""
@ -9420,6 +9420,9 @@ msgstr ""
msgid "Best Regards,"
msgstr ""
msgid "Best practices"
msgstr ""
msgid "Beta"
msgstr ""
@ -10410,6 +10413,9 @@ msgstr ""
msgid "Branch rules"
msgstr ""
msgid "Branch settings"
msgstr ""
msgid "Branch target"
msgstr ""
@ -15619,6 +15625,9 @@ msgstr ""
msgid "ComplianceStandardsAdherence|A rule is configured to require two approvals."
msgstr ""
msgid "ComplianceStandardsAdherence|API Security testing identifies vulnerabilities specific to your application APIs before they can be exploited"
msgstr ""
msgid "ComplianceStandardsAdherence|All attached frameworks"
msgstr ""
@ -15628,12 +15637,36 @@ msgstr ""
msgid "ComplianceStandardsAdherence|At least two approvals"
msgstr ""
msgid "ComplianceStandardsAdherence|Change your project visibility settings to comply with organizational security requirements"
msgstr ""
msgid "ComplianceStandardsAdherence|Check"
msgstr ""
msgid "ComplianceStandardsAdherence|Checks"
msgstr ""
msgid "ComplianceStandardsAdherence|Code quality scanning identifies maintainability issues that could lead to increased security risks over time"
msgstr ""
msgid "ComplianceStandardsAdherence|Configure DAST in your CI/CD pipeline to automatically test your application for security issues"
msgstr ""
msgid "ComplianceStandardsAdherence|Configure DAST scanning"
msgstr ""
msgid "ComplianceStandardsAdherence|Configure approval requirements"
msgstr ""
msgid "ComplianceStandardsAdherence|Configure your project settings to prevent merge request authors from approving their own changes"
msgstr ""
msgid "ComplianceStandardsAdherence|Configure your project to require at least two approvals on merge requests to improve code quality and security"
msgstr ""
msgid "ComplianceStandardsAdherence|Container scanning checks your container images for known vulnerabilities to prevent exploitation in production"
msgstr ""
msgid "ComplianceStandardsAdherence|DAST scan"
msgstr ""
@ -15646,6 +15679,15 @@ msgstr ""
msgid "ComplianceStandardsAdherence|Date since last status change"
msgstr ""
msgid "ComplianceStandardsAdherence|Default branch protection prevents direct commits and ensures changes are reviewed through merge requests"
msgstr ""
msgid "ComplianceStandardsAdherence|Dependency scanning identifies vulnerable dependencies in your project that could be exploited by attackers"
msgstr ""
msgid "ComplianceStandardsAdherence|Dynamic Application Security Testing (DAST) identifies runtime vulnerabilities by analyzing your application while it runs"
msgstr ""
msgid "ComplianceStandardsAdherence|Enable DAST scanner"
msgstr ""
@ -15658,12 +15700,42 @@ msgstr ""
msgid "ComplianceStandardsAdherence|Enable SAST scanner in the project's security configuration to satisfy this requirement."
msgstr ""
msgid "ComplianceStandardsAdherence|Enable code quality scanning"
msgstr ""
msgid "ComplianceStandardsAdherence|Enable code quality scanning to improve code maintainability and reduce technical debt"
msgstr ""
msgid "ComplianceStandardsAdherence|Enable dependency scanning"
msgstr ""
msgid "ComplianceStandardsAdherence|Enable dependency scanning to automatically detect vulnerable libraries in your application"
msgstr ""
msgid "ComplianceStandardsAdherence|Enforcing minimum approval requirements ensures code changes are properly reviewed before merging"
msgstr ""
msgid "ComplianceStandardsAdherence|Ensuring that code committers cannot approve their contributed merge requests maintains separation of duties"
msgstr ""
msgid "ComplianceStandardsAdherence|External control"
msgstr ""
msgid "ComplianceStandardsAdherence|Failed"
msgstr ""
msgid "ComplianceStandardsAdherence|Failed controls: %{failed}"
msgstr ""
msgid "ComplianceStandardsAdherence|Failure reason"
msgstr ""
msgid "ComplianceStandardsAdherence|Filter by"
msgstr ""
msgid "ComplianceStandardsAdherence|Fix available"
msgstr ""
msgid "ComplianceStandardsAdherence|Fix suggestions"
msgstr ""
@ -15673,6 +15745,9 @@ msgstr ""
msgid "ComplianceStandardsAdherence|Frameworks"
msgstr ""
msgid "ComplianceStandardsAdherence|Fuzz testing automatically generates random inputs to find unexpected behavior and potential security issues"
msgstr ""
msgid "ComplianceStandardsAdherence|Group by"
msgstr ""
@ -15697,9 +15772,33 @@ msgstr ""
msgid "ComplianceStandardsAdherence|How to fix"
msgstr ""
msgid "ComplianceStandardsAdherence|Implement API security testing"
msgstr ""
msgid "ComplianceStandardsAdherence|Implement API security testing to protect your application interfaces from attacks"
msgstr ""
msgid "ComplianceStandardsAdherence|Implement secret detection"
msgstr ""
msgid "ComplianceStandardsAdherence|Implement secret detection scanning in your CI/CD pipeline to identify and remove exposed credentials"
msgstr ""
msgid "ComplianceStandardsAdherence|Infrastructure as Code (IaC) scanning detects misconfigurations in your infrastructure definitions before deployment"
msgstr ""
msgid "ComplianceStandardsAdherence|Last scanned"
msgstr ""
msgid "ComplianceStandardsAdherence|Learn about code review best practices"
msgstr ""
msgid "ComplianceStandardsAdherence|Learn more about implementing effective code review practices to enhance security"
msgstr ""
msgid "ComplianceStandardsAdherence|License compliance scanning identifies potentially problematic open source licenses that could create legal issues"
msgstr ""
msgid "ComplianceStandardsAdherence|Merge request approval rules"
msgstr ""
@ -15724,21 +15823,39 @@ msgstr ""
msgid "ComplianceStandardsAdherence|None"
msgstr ""
msgid "ComplianceStandardsAdherence|Organization policy requires that projects are not set to internal visibility to protect sensitive data"
msgstr ""
msgid "ComplianceStandardsAdherence|Other compliance frameworks applied to %{linkStart}%{projectName}%{linkEnd}"
msgstr ""
msgid "ComplianceStandardsAdherence|Passed"
msgstr ""
msgid "ComplianceStandardsAdherence|Passed controls: %{passed}"
msgstr ""
msgid "ComplianceStandardsAdherence|Pending"
msgstr ""
msgid "ComplianceStandardsAdherence|Pending controls: %{pending}"
msgstr ""
msgid "ComplianceStandardsAdherence|Prevent author approvals"
msgstr ""
msgid "ComplianceStandardsAdherence|Prevent authors as approvers"
msgstr ""
msgid "ComplianceStandardsAdherence|Prevent committer approvals"
msgstr ""
msgid "ComplianceStandardsAdherence|Prevent committers as approvers"
msgstr ""
msgid "ComplianceStandardsAdherence|Preventing authors from approving their own merge requests ensures independent code review"
msgstr ""
msgid "ComplianceStandardsAdherence|Project"
msgstr ""
@ -15757,6 +15874,12 @@ msgstr ""
msgid "ComplianceStandardsAdherence|Requirements"
msgstr ""
msgid "ComplianceStandardsAdherence|Review best practices for secure container deployments"
msgstr ""
msgid "ComplianceStandardsAdherence|Review container security practices"
msgstr ""
msgid "ComplianceStandardsAdherence|SAST scan"
msgstr ""
@ -15766,12 +15889,39 @@ msgstr ""
msgid "ComplianceStandardsAdherence|SAST scanner is not configured in the pipeline configuration for the default branch."
msgstr ""
msgid "ComplianceStandardsAdherence|Secret detection prevents sensitive information like API keys from being accidentally committed to your repository"
msgstr ""
msgid "ComplianceStandardsAdherence|Set up branch protection"
msgstr ""
msgid "ComplianceStandardsAdherence|Set up branch protection rules for your default branch to enforce quality standards"
msgstr ""
msgid "ComplianceStandardsAdherence|Set up container scanning"
msgstr ""
msgid "ComplianceStandardsAdherence|Set up container scanning in your pipeline to identify vulnerabilities in your container images"
msgstr ""
msgid "ComplianceStandardsAdherence|Set up fuzz testing"
msgstr ""
msgid "ComplianceStandardsAdherence|Set up fuzz testing in your pipeline to identify edge cases and potential crashes"
msgstr ""
msgid "ComplianceStandardsAdherence|Single Sign-On authentication improves security by centralizing user access management"
msgstr ""
msgid "ComplianceStandardsAdherence|Standard"
msgstr ""
msgid "ComplianceStandardsAdherence|Standards"
msgstr ""
msgid "ComplianceStandardsAdherence|Static Application Security Testing (SAST) scans your code for vulnerabilities that may lead to exploits"
msgstr ""
msgid "ComplianceStandardsAdherence|Status"
msgstr ""
@ -15781,12 +15931,21 @@ msgstr ""
msgid "ComplianceStandardsAdherence|The following features help satisfy this requirement."
msgstr ""
msgid "ComplianceStandardsAdherence|This is an external control for %{link}"
msgstr ""
msgid "ComplianceStandardsAdherence|Unable to load the standards adherence report. Refresh the page and try again."
msgstr ""
msgid "ComplianceStandardsAdherence|Update approval settings in the project's merge request settings to satisfy this requirement."
msgstr ""
msgid "ComplianceStandardsAdherence|Update project visibility"
msgstr ""
msgid "ComplianceStandardsAdherence|Update your approval settings to prevent committers from approving merge requests containing their commits"
msgstr ""
msgid "ComplianceStandardsAdherence|View details"
msgstr ""
@ -15853,6 +16012,9 @@ msgstr ""
msgid "Configuration help"
msgstr ""
msgid "Configuration options"
msgstr ""
msgid "Configure"
msgstr ""
@ -15928,6 +16090,9 @@ msgstr ""
msgid "Configure advanced permissions, Large File Storage, two-factor authentication, and customer relations settings."
msgstr ""
msgid "Configure approval rules"
msgstr ""
msgid "Configure checkin reminder frequency"
msgstr ""
@ -15943,6 +16108,9 @@ msgstr ""
msgid "Configure it later"
msgstr ""
msgid "Configure now"
msgstr ""
msgid "Configure pipeline"
msgstr ""
@ -23326,6 +23494,9 @@ msgstr ""
msgid "Enter a number from 0 to 100."
msgstr ""
msgid "Enter a prompt"
msgstr ""
msgid "Enter a search query to find more branches, or use * to create a wildcard."
msgstr ""
@ -26279,6 +26450,9 @@ msgstr ""
msgid "General settings"
msgstr ""
msgid "Generate"
msgstr ""
msgid "Generate API key at %{site}"
msgstr ""
@ -28103,6 +28277,9 @@ msgstr ""
msgid "Go to the milestone list"
msgstr ""
msgid "Go to the pipeline editor"
msgstr ""
msgid "Go to the project's activity feed"
msgstr ""
@ -30661,6 +30838,12 @@ msgstr ""
msgid "Impersonation tokens"
msgstr ""
msgid "Implementation details"
msgstr ""
msgid "Implementation guide"
msgstr ""
msgid "Import"
msgstr ""
@ -31565,6 +31748,9 @@ msgstr ""
msgid "Insert code"
msgstr ""
msgid "Insert code change summary"
msgstr ""
msgid "Insert column left"
msgstr ""
@ -35605,6 +35791,9 @@ msgstr ""
msgid "Manage usage"
msgstr ""
msgid "Manage visibility"
msgstr ""
msgid "Manage your subscription"
msgstr ""
@ -44568,6 +44757,9 @@ msgstr ""
msgid "Please follow the Let's Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}."
msgstr ""
msgid "Please match the requested format."
msgstr ""
msgid "Please provide a name"
msgstr ""
@ -46172,6 +46364,9 @@ msgstr ""
msgid "Project security status help page"
msgstr ""
msgid "Project settings"
msgstr ""
msgid "Project settings were successfully updated."
msgstr ""
@ -48701,6 +48896,15 @@ msgstr ""
msgid "Re-request review"
msgstr ""
msgid "Reachability|Not available"
msgstr ""
msgid "Reachability|Not found"
msgstr ""
msgid "Reachability|Yes"
msgstr ""
msgid "Read documentation"
msgstr ""
@ -48835,6 +49039,9 @@ msgstr[1] ""
msgid "Refreshing…"
msgstr ""
msgid "Regenerate"
msgstr ""
msgid "Regenerate export"
msgstr ""
@ -52942,6 +53149,9 @@ msgstr ""
msgid "Security reports last updated"
msgstr ""
msgid "Security settings"
msgstr ""
msgid "SecurityApprovals|A merge request approval is required when test coverage declines."
msgstr ""
@ -56032,6 +56242,9 @@ msgstr ""
msgid "Settings|What is experiment?"
msgstr ""
msgid "Setup guide"
msgstr ""
msgid "Severity"
msgstr ""
@ -62939,6 +63152,9 @@ msgstr ""
msgid "UTC"
msgstr ""
msgid "Ultimate"
msgstr ""
msgid "Unable to apply suggestions to a deleted line."
msgstr ""
@ -65382,6 +65598,9 @@ msgstr ""
msgid "View trigger token usage examples"
msgstr ""
msgid "View tutorial"
msgstr ""
msgid "View usage details"
msgstr ""
@ -66003,6 +66222,9 @@ msgstr ""
msgid "Vulnerability|Stacktrace snippet:"
msgstr ""
msgid "Vulnerability|Static reachability is a factor of dependencies (and the relevant CVEs) which indicates that there is a high probability that the package is in use, and therefore the risk of it is higher."
msgstr ""
msgid "Vulnerability|Status"
msgstr ""
@ -66069,6 +66291,9 @@ msgstr ""
msgid "Vulnerability|What is EPSS?"
msgstr ""
msgid "Vulnerability|What is Reachability?"
msgstr ""
msgid "Vulnerability|What is code flow?"
msgstr ""
@ -68307,6 +68532,9 @@ msgstr ""
msgid "Write milestone description…"
msgstr ""
msgid "Write with GitLab Duo"
msgstr ""
msgid "Write your release notes or drag your files here…"
msgstr ""

View File

@ -41,6 +41,7 @@
"preinstall": "node ./scripts/frontend/preinstall.mjs",
"postinstall": "node ./scripts/frontend/postinstall.js",
"storybook:install": "yarn --cwd ./storybook install",
"storybook:doctor": "yarn --cwd ./storybook doctor",
"storybook:build": "yarn tailwindcss:build && yarn --cwd ./storybook build --quiet",
"storybook:start": "./scripts/frontend/start_storybook.sh",
"storybook:start:skip-fixtures-update": "./scripts/frontend/start_storybook.sh --skip-fixtures-update",

View File

@ -22,6 +22,10 @@ module QA
element 'add-issue-field'
end
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
element 'markdown-editor-form-field'
end
base.view 'app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue' do
element 'issue-author'
end

View File

@ -14,6 +14,10 @@ module QA
element 'work-item-title', required: true
end
base.view 'app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue' do
element 'markdown-editor-form-field'
end
base.view 'app/assets/javascripts/work_items/components/work_item_created_updated.vue' do
element 'work-item-author'
end

View File

@ -18,10 +18,20 @@ module QA
element 'remove-related-issue-button'
end
view 'app/assets/javascripts/issues/show/components/description.vue' do
element 'gfm-content'
end
view 'app/assets/javascripts/issues/show/components/edit_actions.vue' do
element 'issuable-save-button'
end
view 'app/assets/javascripts/issues/show/components/header_actions.vue' do
element 'toggle-issue-state-button'
element 'desktop-dropdown'
element 'delete-issue-button'
element 'desktop-dropdown'
element 'edit-button'
element 'issue-header'
element 'toggle-issue-state-button'
end
view 'app/assets/javascripts/related_issues/components/related_issues_block.vue' do
@ -37,6 +47,15 @@ module QA
Page::Project::Issue::Index.perform(&:work_item_enabled?)
end
def edit_description(new_description)
within_element('issue-header') do
click_element('edit-button')
end
fill_element('markdown-editor-form-field', new_description)
click_element('issuable-save-button')
end
def relate_issue(issue)
click_element('crud-form-toggle')
fill_element('add-issue-field', issue.web_url)
@ -63,6 +82,10 @@ module QA
click_element('toggle-issue-state-button', text: 'Close issue')
end
def has_description?(description)
find_element('gfm-content').text.include?(description)
end
def has_reopen_issue_button?
open_actions_dropdown
has_element?('toggle-issue-state-button', text: 'Reopen issue')

View File

@ -13,23 +13,10 @@ module QA
element 'crud-loading'
end
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue' do
element 'link-item-add-button'
end
view 'app/assets/javascripts/work_items/components/shared/work_item_token_input.vue' do
element 'work-item-token-select-input'
end
view "app/assets/javascripts/work_items/components/" \
"work_item_relationships/work_item_add_relationship_form.vue" do
element 'link-work-item-button'
end
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue' do
element 'work-item-linked-items-list'
end
view 'app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue' do
element 'remove-work-item-link'
end
@ -40,6 +27,32 @@ module QA
element 'state-toggle-action'
end
view 'app/assets/javascripts/work_items/components/work_item_description.vue' do
element 'save-description'
element 'work-item-description-wrapper'
end
view 'app/assets/javascripts/work_items/components/work_item_description_rendered.vue' do
element 'work-item-description'
end
view 'app/assets/javascripts/work_items/components/work_item_detail.vue' do
element 'work-item-edit-form-button'
end
view "app/assets/javascripts/work_items/components/" \
"work_item_relationships/work_item_add_relationship_form.vue" do
element 'link-work-item-button'
end
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue' do
element 'link-item-add-button'
end
view 'app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue' do
element 'work-item-linked-items-list'
end
view 'app/assets/javascripts/work_items/components/work_item_title.vue' do
element 'work-item-title'
end
@ -48,6 +61,22 @@ module QA
element 'work-item-feedback-popover'
end
def edit_description(new_description)
close_new_issue_popover if has_element?('work-item-feedback-popover')
wait_for_requests
click_element('work-item-edit-form-button')
within_element('work-item-description-wrapper') do
fill_element('markdown-editor-form-field', new_description)
click_element('save-description')
end
end
def has_description?(description)
find_element('work-item-description').text.include?(description)
end
def has_delete_issue_button?
open_actions_dropdown

View File

@ -54,6 +54,22 @@ module QA
end
end
# See https://gitlab.com/gitlab-org/gitlab/-/issues/526755
it(
'creates an issue and updates the description',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/533855'
) do
resource, _, show_page_type = create_new_issue
updated_description = "Updated issue description"
resource.visit!
show_page_type.perform do |show|
show.edit_description(updated_description)
expect(show).to have_description(updated_description)
end
end
context 'when using attachments in comments', :object_storage do
let(:png_file_name) { 'testfile.png' }
let(:file_to_attach) { Runtime::Path.fixture('designs', png_file_name) }

View File

@ -212,6 +212,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
'LICENSE',
'Pipfile.lock',
'storybook/.env.template',
'storybook/.babelrc.json',
'yarn-error.log'
] +
Dir.glob('.bundle/**/*') +

View File

@ -118,7 +118,8 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat
expect(page).to have_content('pipeline schedule')
within_testid('next-run-cell') do
expect(find('time')['title']).to include(pipeline_schedule.real_next_run.strftime('%B %-d, %Y'))
# validate the format instead of the actual time because timezone issues were causing flaky tests
expect(find('time')['title']).to match(/[A-Z][a-z]+ \d+, \d{4} at \d+:\d+:\d+ [AP]M [A-Z]{3,4}/)
end
expect(page).to have_link('master')
@ -210,6 +211,7 @@ RSpec.describe 'Pipeline Schedules', :js, feature_category: :continuous_integrat
end
it 'prevents an invalid form from being submitted' do
fill_in 'schedule-description', with: 'my fancy description'
create_pipeline_schedule
expect(page).to have_content("Cron timezone can't be blank")

View File

@ -67,6 +67,110 @@ RSpec.describe Groups::UserGroupsFinder, feature_category: :groups_and_projects
end
end
context 'when sorting results' do
context 'when sorting by name' do
context 'in ascending order' do
let(:arguments) { { sort: :name_asc } }
it 'sorts the groups by name in ascending order' do
is_expected.to eq(
[
public_maintainer_group,
public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
]
)
end
end
context 'in descending order' do
let(:arguments) { { sort: :name_desc } }
it 'sorts the groups by name in descending order' do
is_expected.to eq(
[
guest_group,
public_developer_group,
private_maintainer_group,
public_owner_group,
public_maintainer_group
]
)
end
end
end
context 'when sorting by path' do
context 'in ascending order' do
let(:arguments) { { sort: :path_asc } }
it 'sorts the groups by path in ascending order' do
is_expected.to eq(
[
public_maintainer_group,
public_owner_group,
private_maintainer_group,
public_developer_group,
guest_group
]
)
end
end
context 'in descending order' do
let(:arguments) { { sort: :path_desc } }
it 'sorts the groups by path in descending order' do
is_expected.to eq(
[
guest_group,
public_developer_group,
private_maintainer_group,
public_owner_group,
public_maintainer_group
]
)
end
end
end
context 'when sorting by ID' do
context 'in ascending order' do
let(:arguments) { { sort: :id_asc } }
it 'sorts the groups by ID in ascending order' do
is_expected.to eq(
[
guest_group,
private_maintainer_group,
public_developer_group,
public_maintainer_group,
public_owner_group
]
)
end
end
context 'in descending order' do
let(:arguments) { { sort: :id_desc } }
it 'sorts the groups by ID in descending order' do
is_expected.to eq(
[
public_owner_group,
public_maintainer_group,
public_developer_group,
private_maintainer_group,
guest_group
]
)
end
end
end
end
it 'returns all groups where the user is a direct member' do
is_expected.to contain_exactly(
public_maintainer_group,

View File

@ -51,10 +51,6 @@ describe('InputsAdoptionAlert', () => {
it('sets the correct props', () => {
expect(findAlert().props()).toMatchObject({
variant: 'tip',
primaryButtonLink: defaultProvide.pipelineEditorPath,
primaryButtonText: 'Go to the pipeline editor',
secondaryButtonLink: '/help/ci/yaml/inputs',
secondaryButtonText: 'Learn more',
});
});
});

View File

@ -23,6 +23,15 @@ export const mockPipelineInputsResponse = {
options: [],
regex: null,
},
{
name: 'tags',
description: 'Tags for deployment',
default: '',
type: 'ARRAY',
required: false,
options: [],
regex: null,
},
],
__typename: 'Project',
},

View File

@ -31,7 +31,7 @@ describe('PipelineInputsForm', () => {
let wrapper;
let pipelineInputsHandler;
const createComponent = ({ props = {}, provide = {} } = {}) => {
const createComponent = async ({ props = {}, provide = {} } = {}) => {
const handlers = [[getPipelineInputsQuery, pipelineInputsHandler]];
const mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(PipelineInputsForm, {
@ -45,6 +45,7 @@ describe('PipelineInputsForm', () => {
},
apolloProvider: mockApollo,
});
await waitForPromises();
};
const findSkeletonLoader = () => wrapper.findComponent(InputsTableSkeletonLoader);
@ -104,12 +105,21 @@ describe('PipelineInputsForm', () => {
options: [],
regex: null,
},
{
name: 'tags',
description: 'Tags for deployment',
default: '',
type: 'ARRAY',
required: false,
options: [],
regex: null,
},
];
expect(findInputsTable().props('inputs')).toEqual(expectedInputs);
});
it('updates the count in the crud component', () => {
expect(findCrudComponent().props('count')).toBe(2);
expect(findCrudComponent().props('count')).toBe(3);
});
});
@ -135,7 +145,6 @@ describe('PipelineInputsForm', () => {
it('handles GraphQL error', async () => {
await createComponent();
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching the pipeline inputs.',
@ -161,7 +170,6 @@ describe('PipelineInputsForm', () => {
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
const savedInputs = [{ name: 'deploy_environment', value: 'saved-value' }];
await createComponent({ props: { savedInputs } });
await waitForPromises();
const updatedInput = findInputsTable()
.props('inputs')
@ -174,7 +182,6 @@ describe('PipelineInputsForm', () => {
it('processes and emits update events from the table component', async () => {
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
await createComponent();
await waitForPromises();
const updatedInput = { ...wrapper.vm.inputs[0], value: 'updated-value' };
findInputsTable().vm.$emit('update', updatedInput);
@ -190,5 +197,49 @@ describe('PipelineInputsForm', () => {
}));
expect(wrapper.emitted()['update-inputs'][0][0]).toEqual(expectedEmittedValue);
});
it('converts string values to arrays for ARRAY type inputs', async () => {
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
await createComponent();
// Get the array input from the current inputs prop of the table
const inputs = findInputsTable().props('inputs');
const arrayInput = inputs.find((input) => input.type === 'ARRAY');
const updatedInput = {
...arrayInput,
default: '[1,2,3]',
};
findInputsTable().vm.$emit('update', updatedInput);
// Check that the emitted value contains the converted array
const emittedValues = wrapper.emitted()['update-inputs'][0][0];
const emittedArrayValue = emittedValues.find((item) => item.name === 'tags').value;
expect(Array.isArray(emittedArrayValue)).toBe(true);
expect(emittedArrayValue).toEqual([1, 2, 3]);
});
it('converts complex object arrays correctly', async () => {
pipelineInputsHandler = jest.fn().mockResolvedValue(mockPipelineInputsResponse);
await createComponent();
const inputs = findInputsTable().props('inputs');
const arrayInput = inputs.find((input) => input.type === 'ARRAY');
const updatedInput = {
...arrayInput,
default: '[{"key": "value"}, {"another": "object"}]',
};
findInputsTable().vm.$emit('update', updatedInput);
const emittedValues = wrapper.emitted()['update-inputs'][0][0];
const emittedArrayValue = emittedValues.find((item) => item.name === 'tags').value;
expect(Array.isArray(emittedArrayValue)).toBe(true);
expect(emittedArrayValue).toEqual([{ key: 'value' }, { another: 'object' }]);
});
});
});

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlButton, GlDisclosureDropdown, GlDropdownDivider, GlLoadingIcon } from '@gitlab/ui';
import { GlButton, GlDisclosureDropdown, GlLoadingIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/alert';
@ -46,7 +46,7 @@ describe('PipelineStageDropdown', () => {
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findDropdownButton = () => wrapper.findComponent(GlButton);
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findDropdownGroupJobs = () => wrapper.findByTestId('passed-jobs');
const findJobDropdownItems = () => wrapper.findAllComponents(JobDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findStageDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
@ -175,7 +175,7 @@ describe('PipelineStageDropdown', () => {
});
it('renders divider', () => {
expect(findDropdownDivider().exists()).toBe(true);
expect(findDropdownGroupJobs().attributes('class')).toContain('gl-border-t-dropdown-divider');
});
});
@ -197,7 +197,7 @@ describe('PipelineStageDropdown', () => {
});
it('does not render divider', () => {
expect(findDropdownDivider().exists()).toBe(false);
expect(findDropdownGroupJobs().props('bordered')).toBe(false);
});
});

View File

@ -12,6 +12,7 @@ import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_
import { VARIABLE_TYPE } from '~/ci/pipeline_new/constants';
import InputsAdoptionBanner from '~/ci/common/pipeline_inputs/inputs_adoption_banner.vue';
import PipelineVariablesForm from '~/ci/pipeline_new/components/pipeline_variables_form.vue';
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
Vue.use(VueApollo);
jest.mock('~/ci/utils');
@ -49,6 +50,19 @@ describe('PipelineVariablesForm', () => {
},
];
const configVariablesWithMarkdown = [
{
key: 'VAR_WITH_MARKDOWN',
value: 'some-value',
description: 'Variable with **Markdown** _description_',
},
{
key: 'SIMPLE_VAR',
value: 'simple-value',
description: 'Simple variable',
},
];
const createComponent = async ({
props = {},
configVariables = [],
@ -85,6 +99,7 @@ describe('PipelineVariablesForm', () => {
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row-container');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key-field');
const findRemoveButton = () => wrapper.findByTestId('remove-ci-variable-row');
const findMarkdown = () => wrapper.findComponent(Markdown);
beforeEach(() => {
mockCiConfigVariables = jest.fn().mockResolvedValue({
@ -186,6 +201,23 @@ describe('PipelineVariablesForm', () => {
const emptyRowExists = keyInputs.wrappers.some((w) => w.props('value') === '');
expect(emptyRowExists).toBe(true);
});
it('renders markdown if variable has description', async () => {
await createComponent({ configVariables: configVariablesWithMarkdown });
expect(findMarkdown().exists()).toBe(true);
expect(findMarkdown().props('markdown')).toBe('Variable with **Markdown** _description_');
});
it('does not render anything when description is missing', async () => {
await createComponent({
props: {
variableParams: { CUSTOM_VAR: 'custom-value' },
},
});
expect(findMarkdown().exists()).toBe(false);
});
});
describe('query configuration', () => {

View File

@ -38,6 +38,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
queryToObject: jest.fn().mockReturnValue({ id: '1' }),
}));
const preventDefault = jest.fn();
const {
data: {
project: {
@ -287,8 +289,7 @@ describe('Pipeline schedules form', () => {
findPipelineVariables().vm.$emit('update-variables', updatedVariables);
findPipelineInputsForm().vm.$emit('update-inputs', updatedInputs);
findSubmitButton().vm.$emit('click');
findForm().vm.$emit('submit', { preventDefault });
await waitForPromises();
expect(createMutationHandlerSuccess).toHaveBeenCalledWith({
@ -311,7 +312,7 @@ describe('Pipeline schedules form', () => {
createComponent({
requestHandlers: [[createPipelineScheduleMutation, createMutationHandlerFailed]],
});
findSubmitButton().vm.$emit('click');
findForm().vm.$emit('submit', { preventDefault });
await waitForPromises();
@ -327,7 +328,7 @@ describe('Pipeline schedules form', () => {
await waitForPromises();
await findSubmitButton().vm.$emit('click');
findForm().vm.$emit('submit', { preventDefault });
expect(createMutationHandlerSuccess).toHaveBeenCalledWith(
expect.objectContaining({
@ -437,7 +438,7 @@ describe('Pipeline schedules form', () => {
findPipelineVariables().vm.$emit('update-variables', updatedVariables);
findPipelineInputsForm().vm.$emit('update-inputs', updatedInputs);
findSubmitButton().vm.$emit('click');
findForm().vm.$emit('submit', { preventDefault });
await waitForPromises();
@ -466,7 +467,7 @@ describe('Pipeline schedules form', () => {
await waitForPromises();
findSubmitButton().vm.$emit('click');
findForm().vm.$emit('submit', { preventDefault });
await waitForPromises();
@ -483,7 +484,7 @@ describe('Pipeline schedules form', () => {
await waitForPromises();
await findSubmitButton().vm.$emit('click');
findForm().vm.$emit('submit', { preventDefault });
expect(updateMutationHandlerSuccess).toHaveBeenCalledWith(
expect.objectContaining({

View File

@ -1,24 +1,29 @@
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import TreeList from '~/diffs/components/tree_list.vue';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { getCookie, removeCookie, setCookie } from '~/lib/utils/common_utils';
import { TREE_LIST_WIDTH_STORAGE_KEY } from '~/diffs/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import store from '~/mr_notes/stores';
import * as types from '~/diffs/store/mutation_types';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('DiffsFileTree', () => {
useLocalStorageSpy();
Vue.use(PiniaVuePlugin);
describe('DiffsFileTree', () => {
let pinia;
let wrapper;
useLocalStorageSpy();
const createComponent = (propsData = {}) => {
wrapper = extendedWrapper(
shallowMount(DiffsFileTree, {
store,
pinia,
propsData,
}),
);
@ -32,6 +37,11 @@ describe('DiffsFileTree', () => {
global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
});
beforeEach(() => {
pinia = createTestingPinia();
useLegacyDiffs();
});
it('re-emits clickFile event', () => {
const obj = {};
createComponent();
@ -40,15 +50,10 @@ describe('DiffsFileTree', () => {
});
it('sets current file on click', () => {
const spy = jest.spyOn(store, 'commit');
const file = { fileHash: 'foo' };
createComponent();
wrapper.findComponent(TreeList).vm.$emit('clickFile', file);
expect(spy).toHaveBeenCalledWith(
`diffs/${types.SET_CURRENT_DIFF_FILE}`,
file.fileHash,
undefined,
);
expect(useLegacyDiffs()[types.SET_CURRENT_DIFF_FILE]).toHaveBeenCalledWith(file.fileHash);
});
describe('size', () => {

View File

@ -1,8 +1,7 @@
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { PiniaVuePlugin } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
import DiffFileRow from '~/diffs/components//diff_file_row.vue';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -10,22 +9,24 @@ import { SET_LINKED_FILE_HASH, SET_TREE_DATA, SET_DIFF_FILES } from '~/diffs/sto
import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
import { sortTree } from '~/ide/stores/utils';
import { isElementClipped } from '~/lib/utils/common_utils';
import { globalAccessorPlugin } from '~/pinia/plugins';
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
import { getDiffFileMock } from 'jest/diffs/mock_data/diff_file';
jest.mock('~/lib/utils/common_utils');
Vue.use(PiniaVuePlugin);
describe('Diffs tree list component', () => {
let wrapper;
let store;
let setRenderTreeListMock;
let pinia;
const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' });
const getFileRow = () => wrapper.findComponent(DiffFileRow);
const findDiffTreeSearch = () => wrapper.findByTestId('diff-tree-search');
Vue.use(Vuex);
const createComponent = ({ hideFileStats = false, ...rest } = {}) => {
wrapper = shallowMountExtended(TreeList, {
store,
pinia,
propsData: { hideFileStats, ...rest },
stubs: {
// eslint will fail if we import the real component
@ -46,39 +47,14 @@ describe('Diffs tree list component', () => {
};
beforeEach(() => {
const { getters, mutations, actions, state } = createStore();
setRenderTreeListMock = jest.fn();
store = new Vuex.Store({
modules: {
diffs: {
namespaced: true,
state: {
isTreeLoaded: true,
diffFiles: ['test'],
addedLines: 10,
removedLines: 20,
mergeRequestDiff: {},
realSize: 20,
...state,
},
getters: {
allBlobs: getters.allBlobs,
flatBlobsList: getters.flatBlobsList,
linkedFile: getters.linkedFile,
fileTree: getters.fileTree,
},
mutations: { ...mutations },
actions: {
toggleTreeOpen: actions.toggleTreeOpen,
setTreeOpen: actions.setTreeOpen,
goToFile: actions.goToFile,
setRenderTreeList: setRenderTreeListMock,
},
},
},
});
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
useLegacyDiffs().isTreeLoaded = true;
useLegacyDiffs().diffFiles = [getDiffFileMock()];
useLegacyDiffs().addedLines = 10;
useLegacyDiffs().addedLines = 20;
useLegacyDiffs().mergeRequestDiff = {};
useLegacyDiffs().realSize = '20';
useLegacyDiffs().setTreeOpen.mockReturnValue();
});
const setupFilesInState = () => {
@ -166,16 +142,14 @@ describe('Diffs tree list component', () => {
},
};
Object.assign(store.state.diffs, {
treeEntries,
tree: [
treeEntries.LICENSE,
{
...treeEntries.app,
tree: [treeEntries.javascript, treeEntries['index.js'], treeEntries['test.rb']],
},
],
});
useLegacyDiffs().treeEntries = treeEntries;
useLegacyDiffs().tree = [
treeEntries.LICENSE,
{
...treeEntries.app,
tree: [treeEntries.javascript, treeEntries['index.js'], treeEntries['test.rb']],
},
];
return treeEntries;
};
@ -247,32 +221,28 @@ describe('Diffs tree list component', () => {
});
it('calls toggleTreeOpen when clicking folder', () => {
jest.spyOn(store, 'dispatch').mockReturnValue(undefined);
getFileRow().vm.$emit('toggleTreeOpen', 'app');
expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
expect(useLegacyDiffs().toggleTreeOpen).toHaveBeenCalledWith('app');
});
it('renders when renderTreeList is false', async () => {
store.state.diffs.renderTreeList = false;
useLegacyDiffs().renderTreeList = false;
await nextTick();
expect(getScroller().props('items')).toHaveLength(5);
});
it('dispatches setTreeOpen with all paths for the current diff file', async () => {
jest.spyOn(store, 'dispatch').mockReturnValue(undefined);
store.state.diffs.currentDiffFileId = 'appjavascriptfile';
useLegacyDiffs().currentDiffFileId = 'appjavascriptfile';
await nextTick();
expect(store.dispatch).toHaveBeenCalledWith('diffs/setTreeOpen', {
expect(useLegacyDiffs().setTreeOpen).toHaveBeenCalledWith({
opened: true,
path: 'app',
});
expect(store.dispatch).toHaveBeenCalledWith('diffs/setTreeOpen', {
expect(useLegacyDiffs().setTreeOpen).toHaveBeenCalledWith({
opened: true,
path: 'app/javascript',
});
@ -284,7 +254,7 @@ describe('Diffs tree list component', () => {
beforeEach(() => {
setupFilesInState();
store.state.diffs.viewedDiffFileIds = viewedDiffFileIds;
useLegacyDiffs().viewedDiffFileIds = viewedDiffFileIds;
});
it('passes the viewedDiffFileIds to the FileTree', async () => {
@ -312,7 +282,7 @@ describe('Diffs tree list component', () => {
const setupFiles = (diffFiles) => {
const { treeEntries, tree } = generateTreeList(diffFiles);
store.commit(`diffs/${SET_TREE_DATA}`, {
useLegacyDiffs()[SET_TREE_DATA]({
treeEntries,
tree: sortTree(tree),
});
@ -327,7 +297,7 @@ describe('Diffs tree list component', () => {
wrapper.element.insertAdjacentHTML('afterbegin', `<div data-file-row="05.txt"><div>`);
isElementClipped.mockReturnValueOnce(true);
wrapper.vm.$refs.scroller.scrollToItem = jest.fn();
store.state.diffs.currentDiffFileId = '05.txt';
useLegacyDiffs().currentDiffFileId = '05.txt';
await nextTick();
expect(wrapper.vm.currentDiffFileId).toBe('05.txt');
@ -349,13 +319,13 @@ describe('Diffs tree list component', () => {
];
const linkFile = (fileHash) => {
store.commit(`diffs/${SET_LINKED_FILE_HASH}`, fileHash);
useLegacyDiffs()[SET_LINKED_FILE_HASH](fileHash);
};
const setupFiles = (diffFiles) => {
const { treeEntries, tree } = generateTreeList(diffFiles);
store.commit(`diffs/${SET_DIFF_FILES}`, diffFiles);
store.commit(`diffs/${SET_TREE_DATA}`, {
useLegacyDiffs()[SET_DIFF_FILES](diffFiles);
useLegacyDiffs()[SET_TREE_DATA]({
treeEntries,
tree: sortTree(tree),
});
@ -396,13 +366,13 @@ describe('Diffs tree list component', () => {
${'list-view-toggle'} | ${false}
${'tree-view-toggle'} | ${true}
`(
'calls setRenderTreeListMock with `$renderTreeList` when clicking $toggle clicked',
'calls setRenderTreeList with `$renderTreeList` when clicking $toggle clicked',
({ toggle, renderTreeList }) => {
createComponent();
wrapper.findByTestId(toggle).vm.$emit('click');
expect(setRenderTreeListMock).toHaveBeenCalledWith(expect.anything(), {
expect(useLegacyDiffs().setRenderTreeList).toHaveBeenCalledWith({
renderTreeList,
});
},
@ -415,7 +385,7 @@ describe('Diffs tree list component', () => {
`(
'sets $selectedToggle as selected when renderTreeList is $renderTreeList',
({ selectedToggle, deselectedToggle, renderTreeList }) => {
store.state.diffs.renderTreeList = renderTreeList;
useLegacyDiffs().renderTreeList = renderTreeList;
createComponent();
@ -427,10 +397,12 @@ describe('Diffs tree list component', () => {
describe('loading state', () => {
const getLoadedFiles = (offset = 1) =>
store.state.diffs.tree.slice(offset).reduce((acc, el) => {
acc[el.fileHash] = true;
return acc;
}, {});
useLegacyDiffs()
.tree.slice(offset)
.reduce((acc, el) => {
acc[el.fileHash] = true;
return acc;
}, {});
beforeEach(() => {
setupFilesInState();

View File

@ -11,6 +11,8 @@ import { initFileBrowser } from '~/rapid_diffs/app/init_file_browser';
import { StreamingError } from '~/rapid_diffs/streaming_error';
import { useDiffsView } from '~/rapid_diffs/stores/diffs_view';
jest.mock('~/lib/graphql');
jest.mock('~/awards_handler');
jest.mock('~/mr_notes/stores');
jest.mock('~/rapid_diffs/app/view_settings');
jest.mock('~/rapid_diffs/app/init_hidden_files_warning');

View File

@ -183,4 +183,12 @@ describe('Repository last commit component', () => {
});
});
});
describe('polling', () => {
it('polls for last commit and pipeline data', () => {
createComponent();
expect(LastCommit.apollo.commit.pollInterval).toBe(30000);
});
});
});

View File

@ -327,38 +327,30 @@ export const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signatu
id: 'gid://gitlab/Project/6',
repository: {
__typename: 'Repository',
paginatedTree: {
__typename: 'TreeConnection',
nodes: [
{
__typename: 'Tree',
lastCommit: {
__typename: 'Commit',
id: 'gid://gitlab/CommitPresenter/123456789',
sha: '123456789',
title: 'Commit title',
titleHtml: 'Commit title',
descriptionHtml: '',
message: '',
webPath: '/commit/123',
authoredDate: '2019-01-01',
authorName: 'Test',
authorGravatar: 'https://test.com',
author: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
name: 'Test',
avatarUrl: 'https://test.com',
webPath: '/test',
},
signature,
pipelines: {
__typename: 'PipelineConnection',
edges: pipelineEdges,
},
},
},
],
lastCommit: {
__typename: 'Commit',
id: 'gid://gitlab/CommitPresenter/123456789',
sha: '123456789',
title: 'Commit title',
titleHtml: 'Commit title',
descriptionHtml: '',
message: '',
webPath: '/commit/123',
authoredDate: '2019-01-01',
authorName: 'Test',
authorGravatar: 'https://test.com',
author: {
__typename: 'UserCore',
id: 'gid://gitlab/User/1',
name: 'Test',
avatarUrl: 'https://test.com',
webPath: '/test',
},
signature,
pipelines: {
__typename: 'PipelineConnection',
edges: pipelineEdges,
},
},
},
},

View File

@ -24,24 +24,22 @@ describe('Work Item Activity/Discussions Filtering', () => {
const createComponent = ({
loading = false,
workItemType = 'Task',
sortFilterProp = ASC,
sortFilter = ASC,
items = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
trackingLabel = 'item_track_notes_sorting',
trackingAction = 'work_item_notes_sort_order_changed',
filterEvent = 'changeSort',
defaultSortFilterProp = ASC,
defaultSortFilter = ASC,
storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY,
} = {}) => {
wrapper = shallowMountExtended(WorkItemActivitySortFilter, {
propsData: {
loading,
workItemType,
sortFilterProp,
sortFilter,
items,
trackingLabel,
trackingAction,
filterEvent,
defaultSortFilterProp,
defaultSortFilter,
storageKey,
},
});
@ -85,10 +83,10 @@ describe('Work Item Activity/Discussions Filtering', () => {
expect(findLocalStorageSync().props('storageKey')).toBe(storageKey);
});
it(`emits ${filterEvent} event when local storage input is emitted`, () => {
it(`emits "select" event when local storage input is emitted`, () => {
findLocalStorageSync().vm.$emit('input', newInputOption);
expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]);
expect(wrapper.emitted('select')).toEqual([[newInputOption]]);
});
it('emits tracking event when the a non default dropdown item is clicked', () => {

View File

@ -13,7 +13,6 @@ Vue.use(VueApollo);
describe('Work Item Note Actions', () => {
let wrapper;
const noteId = '1';
const showSpy = jest.fn();
const findReplyButton = () => wrapper.findComponent(ReplyButton);
@ -54,7 +53,6 @@ describe('Work Item Note Actions', () => {
showEdit,
workItemIid: '1',
note: {},
noteId,
showAwardEmoji,
showAssignUnassign,
canReportAbuse,

View File

@ -65,7 +65,6 @@ describe('Work Item Note Awards List', () => {
fullPath,
workItemIid,
note,
isModal: false,
},
apolloProvider,
});

View File

@ -58,7 +58,7 @@ describe('WorkItemNotesActivityHeader component', () => {
it('emits `changeFilter` when filtering discussions', () => {
createComponent();
findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
findActivityFilterDropdown().vm.$emit('select', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
});
@ -68,7 +68,7 @@ describe('WorkItemNotesActivityHeader component', () => {
it('emits `changeSort` when sorting discussions/activity', () => {
createComponent();
findActivitySortDropdown().vm.$emit('changeSort', ASC);
findActivitySortDropdown().vm.$emit('select', ASC);
expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
});

View File

@ -194,7 +194,6 @@ describe('WorkItemDetail component', () => {
hasSubepicsFeature,
fullPath: 'group/project',
groupPath: 'group',
reportAbusePath: '/report/abuse/path',
hasLinkedItemsEpicsFeature,
},
stubs: {

View File

@ -133,7 +133,6 @@ describe('WorkItemNotes component', () => {
workItemId,
workItemIid,
workItemType: 'task',
reportAbusePath: '/report/abuse/path',
isDrawer,
isModal,
isWorkItemConfidential,

View File

@ -28,6 +28,7 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
let(:scope) { nil }
let(:project_path) { nil }
let(:verification_level) { nil }
let(:topics) { [] }
let(:args) do
{
@ -35,7 +36,8 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
sort: sort,
search: search,
scope: scope,
verification_level: verification_level
verification_level: verification_level,
topics: topics
}.compact
end
@ -107,6 +109,95 @@ RSpec.describe Resolvers::Ci::Catalog::ResourcesResolver, feature_category: :pip
expect(result.items.pluck(:name)).to contain_exactly('public')
end
end
context 'with topics argument' do
let_it_be(:topic_ruby) { create(:topic, name: 'ruby') }
let_it_be(:topic_rails) { create(:topic, name: 'rails') }
let_it_be(:topic_gitlab) { create(:topic, name: 'gitlab') }
before_all do
create(:project_topic, project: public_namespace_project, topic: topic_ruby)
create(:project_topic, project: public_namespace_project, topic: topic_rails)
create(:project_topic, project: internal_project, topic: topic_gitlab)
end
context 'when filtering by multiple topics' do
let(:topics) { %w[ruby gitlab] }
it 'returns resources with projects matching any of the given topic names' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to eq(%w[public internal])
end
end
context 'when filtering by a single topic' do
let(:topics) { %w[ruby] }
it 'returns resources with projects matching the topic name' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to contain_exactly('public')
end
end
context 'when combining with other filters' do
context 'with search' do
let(:topics) { %w[ruby] }
let(:search) { 'Test' }
it 'returns resources matching both filters' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to contain_exactly('public')
end
end
context 'with verification level' do
let(:topics) { %w[ruby] }
let(:verification_level) { :gitlab_maintained }
it 'returns resources matching both filters' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to contain_exactly('public')
end
end
context 'with scope' do
let(:topics) { %w[ruby] }
let(:scope) { 'NAMESPACES' }
it 'returns resources matching both filters' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to contain_exactly('public')
end
end
context 'with sort' do
let(:topics) { %w[ruby gitlab] }
let(:sort) { 'NAME_DESC' }
it 'returns filtered resources in sorted order' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to eq(%w[public internal])
end
end
end
context 'when filtering by non-existent topics' do
let(:topics) { %w[nonexistent] }
it 'returns no resources' do
expect(result.items).to be_empty
end
end
context 'when topics argument is empty' do
let(:topics) { [] }
it 'returns all visible resources' do
ordered_names = result.items.reorder('catalog_resources.name DESC').pluck(:name)
expect(ordered_names).to contain_exactly('public', 'internal', 'z private test')
end
end
end
end
context 'when the user is anonymous' do

View File

@ -136,5 +136,104 @@ RSpec.describe Resolvers::NestedGroupsResolver, feature_category: :groups_and_pr
end
end
end
context 'when sorting results' do
let_it_be(:parent_group) { create(:group, name: 'Parent Group') }
let_it_be(:group_1) { create(:group, parent: parent_group, name: 'C-group_1', path: 'a-group_1') }
let_it_be(:group_2) { create(:group, parent: parent_group, name: 'B-group_2', path: 'c-group_2') }
let_it_be(:group_3) { create(:group, parent: parent_group, name: 'A-group_3', path: 'b-group_3') }
subject { resolve(described_class, obj: parent_group, args: params, ctx: { current_user: user }) }
context 'when sorting by name' do
context 'in ascending order' do
let(:params) { { sort: 'NAME_ASC' } }
it 'sorts the groups by name in ascending order' do
is_expected.to eq(
[
group_3,
group_2,
group_1
]
)
end
end
context 'in descending order' do
let(:params) { { sort: 'NAME_DESC' } }
it 'sorts the groups by name in descending order' do
is_expected.to eq(
[
group_1,
group_2,
group_3
]
)
end
end
end
context 'when sorting by path' do
context 'in ascending order' do
let(:params) { { sort: 'PATH_ASC' } }
it 'sorts the groups by path in ascending order' do
is_expected.to eq(
[
group_1,
group_3,
group_2
]
)
end
end
context 'in descending order' do
let(:params) { { sort: 'PATH_DESC' } }
it 'sorts the groups by path in descending order' do
is_expected.to eq(
[
group_2,
group_3,
group_1
]
)
end
end
end
context 'when sorting by ID' do
context 'in ascending order' do
let(:params) { { sort: 'ID_ASC' } }
it 'sorts the groups by ID in ascending order' do
is_expected.to eq(
[
group_1,
group_2,
group_3
]
)
end
end
context 'in descending order' do
let(:params) { { sort: 'ID_DESC' } }
it 'sorts the groups by ID in descending order' do
is_expected.to eq(
[
group_3,
group_2,
group_1
]
)
end
end
end
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['GroupSort'], feature_category: :groups_and_projects do
specify { expect(described_class.graphql_name).to eq('GroupSort') }
it 'exposes all the existing sort values' do
expect(described_class.values.keys).to include(
*%w[
SIMILARITY
NAME_ASC
NAME_DESC
PATH_ASC
PATH_DESC
ID_ASC
ID_DESC
]
)
end
end

View File

@ -1151,6 +1151,14 @@ RSpec.describe Gitlab::Auth::AuthFinders, feature_category: :system_access do
allow_any_instance_of(described_class).to receive(:access_token).and_return(oauth_access_token)
end
it 'includes the OAuth application ID in the token info' do
validate_and_save_access_token!(reset_token: true)
expect(::Current.token_info).to match(a_hash_including({
token_application_id: oauth_access_token.application_id
}))
end
context 'when reset_token is true' do
it 'reloads the access token before validation' do
expect(oauth_access_token).to receive(:reload)

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedRoutes, feature_category: :groups_and_projects do
let(:organizations) { table(:organizations) }
let(:namespaces) { table(:namespaces) }
let(:routes) { table(:routes) }
let(:organization) { organizations.create!(name: 'Foobar', path: 'path1') }
let!(:namespace) do
namespaces.create!(name: 'Group', type: 'Group', path: 'group', organization_id: organization.id)
end
subject(:background_migration) do
described_class.new(
start_id: routes.minimum(:id),
end_id: routes.maximum(:id),
batch_table: :routes,
batch_column: :id,
sub_batch_size: 1,
pause_ms: 0,
connection: ApplicationRecord.connection
).perform
end
before do
# Remove constraint so we can create invalid records
ApplicationRecord.connection.execute("ALTER TABLE routes DROP CONSTRAINT fk_679ff8213d;")
routes.create!(path: 'route1', source_id: namespace.id, source_type: 'Namespace', namespace_id: namespace.id)
routes.create!(
path: 'orphaned_route', source_id: non_existing_record_id, source_type: 'Namespace',
namespace_id: non_existing_record_id
)
end
after do
# Re-create constraint after the test
ApplicationRecord.connection.execute(<<~SQL)
ALTER TABLE ONLY routes
ADD CONSTRAINT fk_679ff8213d FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE NOT VALID;
SQL
end
describe '#perform' do
it 'deletes the orphaned routes' do
expect { background_migration }.to change { routes.count }.from(2).to(1)
end
end
end

View File

@ -28,5 +28,23 @@ RSpec.describe Gitlab::GrapeLogging::Loggers::TokenLogger do
expect(subject).to eq({ token_id: 1, token_type: "PersonalAccessToken" })
end
end
describe 'when token is available with an OAuth application ID' do
let(:token_type) { "OAuthAccessToken" }
let(:token_application_id) { 1000 }
before do
::Current.token_info = {
token_id: token_id,
token_type: token_type,
token_scopes: [:api],
token_application_id: token_application_id
}
end
it 'adds the token information with OAuth application ID to log parameters' do
expect(subject).to eq({ token_id: 1, token_type: "OAuthAccessToken", token_application_id: 1000 })
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More