Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-04-08 21:09:52 +00:00
parent 352ce61b77
commit 1561d0c1f5
85 changed files with 1072 additions and 199 deletions

View File

@ -194,7 +194,7 @@ workflow:
variables:
PG_VERSION: "14"
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}-golang-${GO_VERSION}-rust-${RUST_VERSION}-node-${NODE_VERSION}-postgresql-${PG_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-yarn-${YARN_VERSION}-graphicsmagick-${GRAPHICSMAGICK_VERSION}"
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ci/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}-golang-${GO_VERSION}-node-${NODE_VERSION}-postgresql-${PG_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}-lfs-${LFS_VERSION}-chrome-${CHROME_VERSION}-yarn-${YARN_VERSION}-graphicsmagick-${GRAPHICSMAGICK_VERSION}"
DEFAULT_JOB_TAG: "gitlab-org"
GITLAB_LARGE_RUNNER_OPTIONAL: "gitlab-org" # overridden just in gitlab-org/gitlab
GLCI_PRODUCTION_ASSETS_RUNNER_OPTIONAL: "gitlab-org-docker" # this is set to GITLAB_LARGE_RUNNER_OPTIONAL on gitlab-org/gitlab and default to docker runners for dind to work correctly

View File

@ -110,7 +110,7 @@ update-ruby-gems-coverage-cache-push:
extends:
- .default-retry
- .ruby-gems-coverage-cache
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}-rust-${RUST_VERSION}
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/ci/${BUILD_OS}-${OS_VERSION}-slim-ruby-${RUBY_VERSION}:rubygems-${RUBYGEMS_VERSION}-git-${GIT_VERSION}
variables:
BUNDLE_WITHOUT: "" # This is to override the variable defined in .gitlab-ci.yml
BUNDLE_ONLY: "coverage"
@ -651,6 +651,11 @@ rspec:merge-auto-explain-logs:
needs: !reference ["rspec:coverage", "needs"]
script:
- scripts/merge-auto-explain-logs
- |
if [[ -f "$RSPEC_AUTO_EXPLAIN_LOG_PATH" ]] && [[ "$CI_PROJECT_PATH" == "gitlab-org/gitlab" ]] && [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
source scripts/gitlab_component_helpers.sh
extract_and_upload_fingerprints
fi
artifacts:
name: auto-explain-logs
expire_in: 31d

View File

@ -0,0 +1,93 @@
<script>
import { GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
I18N_DELETION_PROTECTION,
DEL_ADJ_PERIOD_MAX_LIMIT_ERROR,
DEL_ADJ_PERIOD_MIN_LIMIT_ERROR,
DEL_ADJ_PERIOD_MAX_LIMIT,
DEL_ADJ_PERIOD_MIN_LIMIT,
} from '../constants';
export default {
components: {
GlFormGroup,
GlFormInput,
GlLink,
},
props: {
deletionAdjournedPeriod: {
type: Number,
required: true,
},
},
data() {
return {
formData: {
deletionAdjournedPeriod: this.deletionAdjournedPeriod,
},
invalidFeedback: '',
};
},
i18n: I18N_DELETION_PROTECTION,
helpPath: helpPagePath('administration/settings/visibility_and_access_controls', {
anchor: 'delayed-project-deletion',
}),
inputId: 'application_setting_deletion_adjourned_period',
computed: {
state() {
return this.invalidFeedback !== '' ? false : null;
},
},
methods: {
validate() {
if (this.formData.deletionAdjournedPeriod > DEL_ADJ_PERIOD_MAX_LIMIT) {
this.invalidFeedback = DEL_ADJ_PERIOD_MAX_LIMIT_ERROR;
return;
}
if (this.formData.deletionAdjournedPeriod < DEL_ADJ_PERIOD_MIN_LIMIT) {
this.invalidFeedback = DEL_ADJ_PERIOD_MIN_LIMIT_ERROR;
return;
}
this.invalidFeedback = '';
},
onBlur() {
this.validate();
},
onInvalid() {
this.validate();
this.$refs.formInput.$el.focus();
},
},
};
</script>
<template>
<gl-form-group
:label="$options.i18n.label"
:label-for="$options.inputId"
:state="state"
:invalid-feedback="invalidFeedback"
>
<template #label-description>
<span>{{ $options.i18n.helpText }}</span>
<gl-link :href="$options.helpPath" target="_blank">{{ $options.i18n.learnMore }}</gl-link>
</template>
<div data-testid="deletion_adjourned_period_group" class="gl-flex gl-items-center">
<gl-form-input
:id="$options.inputId"
ref="formInput"
v-model="formData.deletionAdjournedPeriod"
name="application_setting[deletion_adjourned_period]"
data-testid="deletion_adjourned_period"
width="xs"
type="number"
:min="1"
:max="90"
:state="state"
@blur="onBlur"
@invalid.prevent="onInvalid"
/>
<span class="gl-ml-3">{{ $options.i18n.days }}</span>
</div>
</gl-form-group>
</template>

View File

@ -0,0 +1,21 @@
import { __, s__ } from '~/locale';
export const I18N_DELETION_PROTECTION = {
label: s__('DeletionSettings|Deletion protection'),
helpText: s__(
'DeletionSettings|Period that deleted groups and projects will remain restorable for. Personal projects are always deleted immediately.',
),
learnMore: __('Learn more.'),
days: __('days'),
};
export const DEL_ADJ_PERIOD_MAX_LIMIT = 90;
export const DEL_ADJ_PERIOD_MIN_LIMIT = 1;
export const DEL_ADJ_PERIOD_MAX_LIMIT_ERROR = s__(
'DeletionSettings|Maximum deletion protection duration is 90 days.',
);
export const DEL_ADJ_PERIOD_MIN_LIMIT_ERROR = s__(
'DeletionSettings|Minimum deletion protection duration is 1 day.',
);

View File

@ -0,0 +1,25 @@
import Vue from 'vue';
import { parseFormProps } from './utils';
import FormGroup from './components/form_group.vue';
export const initAdminDeletionProtectionSettings = () => {
const el = document.querySelector('#js-admin-deletion-protection-settings');
if (!el) {
return false;
}
const { deletionAdjournedPeriod } = parseFormProps(el.dataset);
return new Vue({
el,
name: 'AdminDeletionProtectionSettings',
render(createElement) {
return createElement(FormGroup, {
props: {
deletionAdjournedPeriod,
},
});
},
});
};

View File

@ -0,0 +1,12 @@
import { parseBoolean } from '~/lib/utils/common_utils';
export const parseFormProps = ({
deletionAdjournedPeriod,
delayedGroupDeletion,
delayedProjectDeletion,
}) => ({
deletionAdjournedPeriod:
deletionAdjournedPeriod !== undefined ? parseInt(deletionAdjournedPeriod, 10) : undefined,
delayedGroupDeletion: parseBoolean(delayedGroupDeletion),
delayedProjectDeletion: parseBoolean(delayedProjectDeletion),
});

View File

@ -23,10 +23,10 @@ export default {
<div>
<gl-card
class="ci-card gl-rounded-lg gl-bg-section"
header-class="gl-rounded-lg gl-px-0 gl-py-0 gl-bg-section gl-border-b-0"
body-class="gl-pt-2 gl-pb-0 gl-px-2"
header-class="gl-rounded-t-lg gl-px-0 gl-pt-0 gl-pb-2 gl-bg-section gl-border-b-section"
body-class="gl-pt-2 gl-pb-0 gl-px-0"
>
<template #header>
<template v-if="$scopedSlots.stages" #header>
<slot name="stages"></slot>
</template>

View File

@ -5,6 +5,7 @@ import { reportToSentry } from '~/ci/utils';
import { __, s__, sprintf } from '~/locale';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { sanitize } from '~/lib/dompurify';
import { FAILED_STATUS } from '~/ci/constants';
import RootGraphLayout from './root_graph_layout.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
@ -114,6 +115,12 @@ export default {
stageName: this.name,
});
},
nonFailedJobs() {
return this.groups.filter((group) => group.status?.group !== FAILED_STATUS);
},
failedJobs() {
return this.groups.filter((group) => group.status?.group === FAILED_STATUS);
},
},
mounted() {
this.$emit('updateMeasurements');
@ -176,7 +183,7 @@ export default {
class="stage-column gl-relative gl-basis-full"
data-testid="stage-column"
>
<template #stages>
<template v-if="name" #stages>
<div
data-testid="stage-column-title"
class="stage-column-title gl-pipeline-job-width gl-relative -gl-mb-2 gl-flex gl-justify-between gl-truncate gl-pl-4 gl-font-bold gl-leading-36"
@ -198,37 +205,87 @@ export default {
</div>
</template>
<template #jobs>
<div v-if="failedJobs.length > 0">
<div
class="gl-my-2 gl-pl-4 gl-text-sm gl-font-bold gl-text-strong"
data-testid="failed-jobs-title"
>
{{ s__('Pipelines|Failed jobs') }}
</div>
<div class="gl-px-2">
<div
v-for="group in failedJobs"
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group-failed"
class="gl-pipeline-job-width gl-relative gl-mb-2 gl-whitespace-normal"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
>
<div v-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
<job-group-dropdown
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
:css-class-job-name="$options.jobClasses"
/>
</div>
<job-item
v-else-if="singleJobExists(group)"
:job="group.jobs[0]"
:job-hovered="jobHovered"
:skip-retry-modal="skipRetryModal"
:source-job-hovered="sourceJobHovered"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
:css-class-job-name="$options.jobClasses"
:class="[{ 'gl-opacity-3': isFadedOut(group.name) }, 'gl-duration-slow gl-ease-ease']"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
</div>
</div>
</div>
<div
v-for="group in groups"
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
class="gl-pipeline-job-width gl-relative gl-mb-2 gl-whitespace-normal"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
v-if="nonFailedJobs.length > 0"
class="gl-px-2"
:class="{ 'gl-border-t': failedJobs.length > 0 }"
>
<div v-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
<job-group-dropdown
:group="group"
:stage-name="showStageName ? group.stageName : ''"
<div
v-for="group in nonFailedJobs"
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
class="gl-pipeline-job-width gl-relative gl-mb-2 gl-whitespace-normal"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
>
<div v-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
<job-group-dropdown
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
:css-class-job-name="$options.jobClasses"
/>
</div>
<job-item
v-else-if="singleJobExists(group)"
:job="group.jobs[0]"
:job-hovered="jobHovered"
:skip-retry-modal="skipRetryModal"
:source-job-hovered="sourceJobHovered"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
:css-class-job-name="$options.jobClasses"
:class="[{ 'gl-opacity-3': isFadedOut(group.name) }, 'gl-duration-slow gl-ease-ease']"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
</div>
<job-item
v-else-if="singleJobExists(group)"
:job="group.jobs[0]"
:job-hovered="jobHovered"
:skip-retry-modal="skipRetryModal"
:source-job-hovered="sourceJobHovered"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
:css-class-job-name="$options.jobClasses"
:class="[{ 'gl-opacity-3': isFadedOut(group.name) }, 'gl-duration-slow gl-ease-ease']"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
@setSkipRetryModal="$emit('setSkipRetryModal')"
/>
</div>
</template>
</root-graph-layout>

View File

@ -36,6 +36,7 @@ export default {
'currentDefinitionPosition',
'currentBlobPath',
'definitionPathPrefix',
'data',
]),
},
mounted() {
@ -50,17 +51,28 @@ export default {
this.body = document.body;
eventHub.$on('showBlobInteractionZones', this.showBlobInteractionZones);
eventHub.$on('showBlobInteractionZones', this.showCodeNavigation);
this.addGlobalEventListeners();
this.fetchData();
},
beforeDestroy() {
eventHub.$off('showBlobInteractionZones', this.showBlobInteractionZones);
eventHub.$off('showBlobInteractionZones', this.showCodeNavigation);
this.removeGlobalEventListeners();
},
methods: {
...mapActions(['fetchData', 'showDefinition', 'showBlobInteractionZones', 'setInitialData']),
showCodeNavigation(path) {
if (this.data?.[path]) {
this.showBlobInteractionZones(path);
} else {
const unwatchData = this.$watch('data', () => {
unwatchData();
this.showBlobInteractionZones(path);
});
}
},
addGlobalEventListeners() {
if (this.body) {
this.body.addEventListener('click', this.showDefinition);

View File

@ -65,10 +65,18 @@ export default {
if (el.closest('.js-code-navigation') && !isCurrentElementPopoverOpen) {
const { lineIndex, charIndex } = el.dataset;
const blobViewer = el.closest('.file-holder')?.querySelector('.blob-viewer');
let offsetLeft = 0;
const blobContent = document.querySelector('.line-numbers')?.nextElementSibling;
if (blobContent?.classList.contains('blob-content')) {
offsetLeft = blobContent.offsetLeft;
}
position = {
x: el.offsetLeft || 0,
y: el.offsetTop + (el.closest('pre.code')?.offsetTop || 0) || 0,
x: (el.offsetLeft || 0) + offsetLeft,
y: el.offsetTop + (blobViewer?.offsetTop || 0) || 0,
height: el.offsetHeight,
lineIndex: parseInt(lineIndex, 10),
};

View File

@ -85,6 +85,15 @@ export const typePolicies = {
OrganizationUserConnection: {
merge: true,
},
RepositoryBlob: {
keyFields: ({ id, path }) => {
if (path) {
return `${id}${encodeURIComponent(path)}`;
}
return id;
},
},
};
export const stripWhitespaceFromQuery = (url, path) => {

View File

@ -1,6 +1,7 @@
import { initSilentModeSettings } from '~/silent_mode_settings';
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import VscodeExtensionMarketplaceSettings from '~/vscode_extension_marketplace/components/settings_app.vue';
import { initAdminDeletionProtectionSettings } from '~/admin/application_settings/deletion_protection';
import initAccountAndLimitsSection from '../account_and_limits';
import initGitpod from '../gitpod';
import initSignupRestrictions from '../signup_restrictions';
@ -10,6 +11,7 @@ import initSignupRestrictions from '../signup_restrictions';
initGitpod();
initSignupRestrictions();
initSilentModeSettings();
initAdminDeletionProtectionSettings();
initSimpleApp('#js-extension-marketplace-settings-app', VscodeExtensionMarketplaceSettings);
})();

View File

@ -15,6 +15,7 @@ import LineHighlighter from '~/blob/line_highlighter';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import highlightMixin from '~/repository/mixins/highlight_mixin';
import projectInfoQuery from 'ee_else_ce/repository/queries/project_info.query.graphql';
import eventHub from '~/notes/event_hub';
import getRefMixin from '../mixins/get_ref';
import { getRefType } from '../utils/ref_type';
import {
@ -292,6 +293,7 @@ export default {
await this.$nextTick();
handleLocationHash(); // Ensures that we scroll to the hash when async content is loaded
eventHub.$emit('showBlobInteractionZones', this.blobInfo.path);
})
.catch(() => this.displayError());
},

View File

@ -132,7 +132,7 @@ export default {
hide-button
is-group
data-testid="new-group-work-item-modal"
:work-item-type-name="$options.WORK_ITEM_TYPE_ENUM_EPIC"
:preselected-work-item-type="$options.WORK_ITEM_TYPE_ENUM_EPIC"
@hideModal="showCreateGroupWorkItemModal = false"
/>
<create-work-item-modal

View File

@ -73,7 +73,7 @@ export default {
required: false,
default: false,
},
workItemTypeName: {
preselectedWorkItemType: {
type: String,
required: false,
default: null,
@ -94,7 +94,7 @@ export default {
return {
isCreateModalVisible: false,
isConfirmationModalVisible: false,
selectedWorkItemTypeName: this.workItemTypeName,
selectedWorkItemTypeName: this.preselectedWorkItemType,
shouldDiscardDraft: false,
};
},
@ -126,7 +126,7 @@ export default {
return newWorkItemPath({
fullPath: this.fullPath,
isGroup: this.isGroup,
workItemTypeName: this.workItemTypeName,
workItemTypeName: this.selectedWorkItemTypeName,
query: this.newWorkItemPathQuery,
});
},
@ -136,7 +136,7 @@ export default {
];
},
newWorkItemButtonText() {
return this.alwaysShowWorkItemTypeSelect && this.workItemTypeName
return this.alwaysShowWorkItemTypeSelect && this.selectedWorkItemTypeName
? sprintfWorkItem(s__('WorkItem|New %{workItemType}'), '')
: this.newWorkItemText;
},
@ -165,7 +165,7 @@ export default {
hideCreateModal() {
this.$emit('hideModal');
this.isCreateModalVisible = false;
this.selectedWorkItemTypeName = this.workItemTypeName;
this.selectedWorkItemTypeName = this.preselectedWorkItemType;
},
showCreateModal(event) {
if (Boolean(event) && isMetaClick(event)) {
@ -198,7 +198,7 @@ export default {
this.hideConfirmationModal();
},
handleDiscardDraft(modal) {
this.selectedWorkItemTypeName = this.workItemTypeName;
this.selectedWorkItemTypeName = this.preselectedWorkItemType;
if (modal === 'createModal') {
// This is triggered on the create modal when the user didn't update the form,
@ -317,7 +317,7 @@ export default {
:parent-id="parentId"
:show-project-selector="showProjectSelector"
:title="title"
:work-item-type-name="workItemTypeName"
:work-item-type-name="selectedWorkItemTypeName"
:related-item="relatedItem"
:should-discard-draft="shouldDiscardDraft"
:is-modal="true"

View File

@ -693,7 +693,7 @@ export default {
@action="handleToggleReportAbuseModal"
>
<template #list-item>
<gl-icon name="review-warning" class="gl-mr-2" variant="subtle" />
<gl-icon name="abuse" class="gl-mr-2" variant="subtle" />
{{ $options.i18n.reportAbuse }}
</template>
</gl-disclosure-dropdown-item>
@ -791,7 +791,7 @@ export default {
:always-show-work-item-type-select="!isGroup"
:visible="isCreateWorkItemModalVisible"
:related-item="relatedItemData"
:work-item-type-name="workItemTypeNameEnum"
:preselected-work-item-type="workItemTypeNameEnum"
:show-project-selector="!isEpic"
:is-group="isGroup"
hide-button

View File

@ -382,7 +382,7 @@ export default {
:show-project-selector="isGroup"
:title="childTitle"
:visible="visible"
:work-item-type-name="childItemType"
:preselected-work-item-type="childItemType"
@hideModal="visible = false"
@workItemCreated="handleWorkItemCreated"
/>

View File

@ -917,7 +917,7 @@ export default {
<template #new-issue-button>
<create-work-item-modal
:is-group="isGroup"
:work-item-type-name="workItemTypeName"
:preselected-work-item-type="workItemTypeName"
@workItemCreated="handleWorkItemCreated"
/>
</template>

View File

@ -197,6 +197,7 @@ module Admin
:lets_encrypt_notification_email,
:lets_encrypt_terms_of_service_accepted,
:domain_denylist_file,
:deletion_adjourned_period,
:raw_blob_request_limit,
:issues_create_limit,
:notes_create_limit,

View File

@ -713,6 +713,12 @@ module ApplicationSettingsHelper
# NOTE: description is overridden in EE
_('Enable VS Code Extension Marketplace and configure the extensions registry for Web IDE.')
end
def deletion_protection_data
{
deletion_adjourned_period: @application_setting[:deletion_adjourned_period]
}
end
end
ApplicationSettingsHelper.prepend_mod_with('ApplicationSettingsHelper')

View File

@ -0,0 +1,3 @@
- return unless Feature.enabled?(:downtier_delayed_deletion, :instance, type: :wip)
#js-admin-deletion-protection-settings{ data: deletion_protection_data }

View File

@ -30,6 +30,9 @@
= _("Time (in hours) that users are allowed to skip forced configuration of two-factor authentication.")
- if @group.namespace_settings.present?
- if @group.licensed_feature_available?(:security_orchestration_policies) && @group.root?
= render_if_exists 'groups/admin_security_policies', form: f
.form-group.gl-form-group
%legend.col-form-label.col-form-label
= s_('Runners|Runner Registration')

View File

@ -6,4 +6,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/522987
milestone: '17.10'
group: group::code review
type: beta
default_enabled: false
default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397730
milestone: '15.11'
type: development
group: group::authentication
default_enabled: false
default_enabled: true

View File

@ -555,6 +555,10 @@ sbom_components:
- table: organizations
column: organization_id
on_delete: async_delete
sbom_graph_paths:
- table: projects
column: project_id
on_delete: async_delete
sbom_occurrences:
- table: p_ci_pipelines
column: pipeline_id

View File

@ -8,14 +8,6 @@ description: Persists links between timeline event tags and timeline events.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/100271
milestone: '15.6'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: timeline_event_tag_id
table: incident_management_timeline_event_tags
sharding_key: project_id
belongs_to: timeline_event_tag
desired_sharding_key_migration_job_name: BackfillIncidentManagementTimelineEventTagLinksProjectId
sharding_key:
project_id: projects
table_size: small

View File

@ -0,0 +1,13 @@
---
table_name: sbom_graph_paths
classes:
- Sbom::GraphPath
feature_categories:
- dependency_management
description: Stores dependency graph paths
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184240
milestone: '17.11'
gitlab_schema: gitlab_sec
sharding_key:
project_id: projects
table_size: small

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class CreateSbomGraphPaths < Gitlab::Database::Migration[2.2]
milestone '17.11'
def change
create_table :sbom_graph_paths do |t|
t.bigint :ancestor_id, null: false
t.bigint :descendant_id, null: false
t.bigint :project_id, null: false
t.integer :path_length, null: false
t.index :ancestor_id
t.index :descendant_id
t.index [:project_id, :descendant_id]
t.timestamps_with_timezone null: false
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddSbomOccurrencesFksToSbomGraphPaths < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.11'
def up
add_concurrent_foreign_key :sbom_graph_paths, :sbom_occurrences, column: :ancestor_id,
on_delete: :cascade
add_concurrent_foreign_key :sbom_graph_paths, :sbom_occurrences, column: :descendant_id,
on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :sbom_graph_paths, column: :ancestor_id
remove_foreign_key :sbom_graph_paths, column: :descendant_id
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddSecurityPoliciesNamespaceSetting < Gitlab::Database::Migration[2.2]
milestone '17.11'
enable_lock_retries!
def change
add_column :namespace_settings, :security_policies, :jsonb, default: {}, null: false
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddSecurityPoliciesHashConstraintToNamespaceSettings < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.11'
CONSTRAINT_NAME = 'check_namespace_settings_security_policies_is_hash'
def up
add_check_constraint(
:namespace_settings,
"(jsonb_typeof(security_policies) = 'object')",
CONSTRAINT_NAME
)
end
def down
remove_check_constraint :namespace_settings, CONSTRAINT_NAME
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddIncidentManagementTimelineEventTagLinksProjectIdNotNull < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.11'
def up
add_not_null_constraint :incident_management_timeline_event_tag_links, :project_id
end
def down
remove_not_null_constraint :incident_management_timeline_event_tag_links, :project_id
end
end

View File

@ -0,0 +1 @@
9c832ec50a8fc33fe133c3c8a9357fa665ead6272cb2cebdb2906b392b032026

View File

@ -0,0 +1 @@
35eafea460167f1b1359c7adf5e432086a521aa5104b487260bb762e9e45aabe

View File

@ -0,0 +1 @@
a60a8786eb1c9752739186163fdc325490b587493d6b8ed915aa772902d77326

View File

@ -0,0 +1 @@
cff376d79d1a029588c473c0f500501b7e24be69a2a9ec7f1131fcaf3a4d2f3e

View File

@ -0,0 +1 @@
368940784a31e8bba5c12d849fba003a87673cbc03f9a96100344c5876855600

View File

@ -15732,7 +15732,8 @@ CREATE TABLE incident_management_timeline_event_tag_links (
timeline_event_id bigint NOT NULL,
timeline_event_tag_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_e693cb4516 CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE incident_management_timeline_event_tag_links_id_seq
@ -18026,7 +18027,9 @@ CREATE TABLE namespace_settings (
jwt_ci_cd_job_token_opted_out boolean DEFAULT false NOT NULL,
require_dpop_for_manage_api_endpoints boolean DEFAULT true NOT NULL,
job_token_policies_enabled boolean DEFAULT false NOT NULL,
security_policies jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT check_namespace_settings_security_policies_is_hash CHECK ((jsonb_typeof(security_policies) = 'object'::text)),
CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)),
CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100))
);
@ -22314,6 +22317,25 @@ CREATE SEQUENCE sbom_components_id_seq
ALTER SEQUENCE sbom_components_id_seq OWNED BY sbom_components.id;
CREATE TABLE sbom_graph_paths (
id bigint NOT NULL,
ancestor_id bigint NOT NULL,
descendant_id bigint NOT NULL,
project_id bigint NOT NULL,
path_length integer NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE sbom_graph_paths_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE sbom_graph_paths_id_seq OWNED BY sbom_graph_paths.id;
CREATE TABLE sbom_occurrences (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -27642,6 +27664,8 @@ ALTER TABLE ONLY sbom_component_versions ALTER COLUMN id SET DEFAULT nextval('sb
ALTER TABLE ONLY sbom_components ALTER COLUMN id SET DEFAULT nextval('sbom_components_id_seq'::regclass);
ALTER TABLE ONLY sbom_graph_paths ALTER COLUMN id SET DEFAULT nextval('sbom_graph_paths_id_seq'::regclass);
ALTER TABLE ONLY sbom_occurrences ALTER COLUMN id SET DEFAULT nextval('sbom_occurrences_id_seq'::regclass);
ALTER TABLE ONLY sbom_occurrences_vulnerabilities ALTER COLUMN id SET DEFAULT nextval('sbom_occurrences_vulnerabilities_id_seq'::regclass);
@ -30617,6 +30641,9 @@ ALTER TABLE ONLY sbom_component_versions
ALTER TABLE ONLY sbom_components
ADD CONSTRAINT sbom_components_pkey PRIMARY KEY (id);
ALTER TABLE ONLY sbom_graph_paths
ADD CONSTRAINT sbom_graph_paths_pkey PRIMARY KEY (id);
ALTER TABLE ONLY sbom_occurrences
ADD CONSTRAINT sbom_occurrences_pkey PRIMARY KEY (id);
@ -36959,6 +36986,12 @@ CREATE INDEX index_sbom_component_versions_on_organization_id ON sbom_component_
CREATE INDEX index_sbom_components_on_organization_id ON sbom_components USING btree (organization_id);
CREATE INDEX index_sbom_graph_paths_on_ancestor_id ON sbom_graph_paths USING btree (ancestor_id);
CREATE INDEX index_sbom_graph_paths_on_descendant_id ON sbom_graph_paths USING btree (descendant_id);
CREATE INDEX index_sbom_graph_paths_on_project_id_and_descendant_id ON sbom_graph_paths USING btree (project_id, descendant_id);
CREATE INDEX index_sbom_occurr_on_project_id_and_component_version_id_and_id ON sbom_occurrences USING btree (project_id, component_version_id, id);
CREATE INDEX index_sbom_occurrences_on_component_id_and_id ON sbom_occurrences USING btree (component_id, id);
@ -43170,6 +43203,9 @@ ALTER TABLE ONLY targeted_message_dismissals
ALTER TABLE ONLY timelogs
ADD CONSTRAINT fk_c49c83dd77 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY sbom_graph_paths
ADD CONSTRAINT fk_c4c7d16f3e FOREIGN KEY (ancestor_id) REFERENCES sbom_occurrences(id) ON DELETE CASCADE;
ALTER TABLE ONLY wiki_repository_states
ADD CONSTRAINT fk_c558ca51b8 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@ -43449,6 +43485,9 @@ ALTER TABLE ONLY fork_networks
ALTER TABLE ONLY packages_conan_package_references
ADD CONSTRAINT fk_e7b5f3afc7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY sbom_graph_paths
ADD CONSTRAINT fk_e83002e9da FOREIGN KEY (descendant_id) REFERENCES sbom_occurrences(id) ON DELETE CASCADE;
ALTER TABLE ONLY error_tracking_error_events
ADD CONSTRAINT fk_e84882273e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -31,7 +31,7 @@ Requests over the rate limit are logged into the `auth.log` file.
For example, if you set a limit of 400 for `POST /organizations`, requests to the API endpoint that
exceed a rate of 400 within one minute are blocked. Access to the endpoint is restored after one minute.
You can configure the per minute rate limit per user for requests to the [POST /organizations API](../../api/organizations.md#create-organization). The default is 10.
You can configure the per minute rate limit per user for requests to the [POST /organizations API](../../api/organizations.md#create-an-organization). The default is 10.
## Change the rate limit

View File

@ -12,6 +12,8 @@ title: Group badges API
{{< /details >}}
Use this API to interact with group badges. For more information, see [group badges](../user/project/badges.md#group-badges).
## Placeholder tokens
[Badges](../user/project/badges.md) support placeholders that are replaced in real time in both the link and image URL. The allowed placeholders are:

View File

@ -12,7 +12,7 @@ title: LDAP group links
{{< /details >}}
List, add, and delete [LDAP group links](../user/group/access_and_permissions.md#manage-group-memberships-with-ldap).
Use this API to manage LDAP group links. For more information, see [manage group memberships with LDAP](../user/group/access_and_permissions.md#manage-group-memberships-with-ldap).
## List LDAP group links

View File

@ -12,7 +12,7 @@ title: Groups API
{{< /details >}}
Use the Groups API to list and manage GitLab groups through REST API calls. For more information, see [groups](../user/group/_index.md).
Use this API to view and manage GitLab groups. For more information, see [groups](../user/group/_index.md).
Endpoint responses might vary based on the [permissions](../user/permissions.md) of the authenticated user in the group.

View File

@ -12,8 +12,7 @@ title: Invitations API
{{< /details >}}
Use the Invitations API to invite or add users to a group or project, and to list pending
invitations.
Use this API to manage invitations and add users to a [group](../user/group/_index.md#add-users-to-a-group) or [project](../user/project/members/_index.md).
## Valid access levels

View File

@ -40,6 +40,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" \
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"created_at": "2015-09-03T07:24:44.627Z",
"expires_at": "2020-05-05T00:00:00.000Z",
"last_used_at": "2020-04-07T00:00:00.000Z",
"usage_type": "auth",
"user": {
"name": "John Smith",
@ -103,6 +104,7 @@ Example response:
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"created_at": "2019-11-14T15:11:13.222Z",
"expires_at": "2020-05-05T00:00:00.000Z",
"last_used_at": "2020-04-07T00:00:00.000Z",
"usage_type": "auth",
"user": {
"id": 1,
@ -168,6 +170,8 @@ Example response:
"title": "Sample key 1",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"created_at": "2019-11-14T15:11:13.222Z",
"expires_at": "2020-05-05T00:00:00.000Z",
"last_used_at": "2020-04-07T00:00:00.000Z",
"usage_type": "auth",
"user": {
"id": 1,

View File

@ -12,7 +12,7 @@ title: Namespaces API
{{< /details >}}
Use this API to interact with namespaces, a special resource category used to organize users and groups. For more information, see [Namespaces](../user/namespace/_index.md).
Use this API to interact with namespaces, a special resource category used to organize users and groups. For more information, see [namespaces](../user/namespace/_index.md).
This API uses [Pagination](rest/_index.md#pagination) to filter results.

View File

@ -12,7 +12,7 @@ title: Notification settings API
{{< /details >}}
Change [notification settings](../user/profile/notifications.md) using the REST API.
Use this API to manage settings for GitLab notifications. For more information, see [notification emails](../user/profile/notifications.md).
## Valid notification levels

View File

@ -53451,6 +53451,10 @@ definitions:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
last_used_at:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
key:
type: string
example: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNJAkI3Wdf0r13c8a5pEExB2YowPWCSVzfZV22pNBc1CuEbyYLHpUyaD0GwpGvFdx2aP7lMEk35k6Rz3ccBF6jRaVJyhsn5VNnW92PMpBJ/P1UebhXwsFHdQf5rTt082cSxWuk61kGWRQtk4ozt/J2DF/dIUVaLvc+z4HomT41fQ==
@ -53518,6 +53522,10 @@ definitions:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
last_used_at:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
key:
type: string
example: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNJAkI3Wdf0r13c8a5pEExB2YowPWCSVzfZV22pNBc1CuEbyYLHpUyaD0GwpGvFdx2aP7lMEk35k6Rz3ccBF6jRaVJyhsn5VNnW92PMpBJ/P1UebhXwsFHdQf5rTt082cSxWuk61kGWRQtk4ozt/J2DF/dIUVaLvc+z4HomT41fQ==
@ -65772,6 +65780,10 @@ definitions:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
last_used_at:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
key:
type: string
example: |-
@ -67329,6 +67341,10 @@ definitions:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
last_used_at:
type: string
format: date-time
example: '2020-09-03T07:24:44.627Z'
key:
type: string
example: |-

View File

@ -13,7 +13,9 @@ title: Organizations API
{{< /details >}}
## Create organization
Use this API to interact with GitLab organizations. For more information, see [organization](../user/organization/_index.md).
## Create an organization
{{< history >}}

View File

@ -12,7 +12,7 @@ title: Project forks API
{{< /details >}}
You can manage [project forks](../user/project/repository/forking_workflow.md) by using the REST API.
Use this API to manage forks of GitLab projects. For more information, see [forks](../user/project/repository/forking_workflow.md).
## Fork a project

View File

@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
title: Project starring API
---
You can get information about [projects and stars](../user/project/working_with_projects.md) by using the REST API.
Use this API to interact with starred projects. For more information, see [projects and stars](../user/project/working_with_projects.md).
## List projects starred by a user

View File

@ -12,7 +12,9 @@ title: Projects API
{{< /details >}}
The Projects API provides programmatic access to manage GitLab projects and configure their key settings. A project is a central hub for collaboration where you store code, track issues, and organize team activities.
Use this API to manage GitLab projects and their associated settings. A project is a central hub for
collaboration where you store code, track issues, and organize team activities.
For more information, see [create a project](../user/project/_index.md).
The Projects API contains endpoints that:
@ -24,8 +26,6 @@ The Projects API contains endpoints that:
- Transfer projects between namespaces
- Manage deployment and container registry settings
This page explains how to use the Projects REST API endpoints to interact with [GitLab projects](../user/project/_index.md).
## Permissions
Users with:

View File

@ -12,7 +12,7 @@ title: GitLab To-Do List API
{{< /details >}}
Interact with [to-do items](../user/todos.md) using the REST API.
Use this API to interact with [to-do items](../user/todos.md).
## Get a list of to-do items

View File

@ -12,7 +12,7 @@ title: Topics API
{{< /details >}}
Interact with project topics using the REST API.
Use this API to interact with project topics. For more information, see [project topics](../user/project/project_topics.md).
## List topics

View File

@ -0,0 +1,23 @@
---
stage: Security Risk Management
group: Security Insights
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
title: SBoM dependency graph ingestion overview
---
## Overview
The process starts *after* all `SBoM::Occurence` models have been ingested because we ingest them in slices and it would be tricky to process that in slices as well.
All work happens in a background worker which will be added in a subsequent MR so that we do not increase the time it takes to ingest an SBoM report. This means that there will be a delay between when the SBoM report is ingested and before the dependency graph is updated.
All record pertaining to dependency graphs are stored in `sbom_graph_paths` database table and has foreign keys to `sbom_occurrences` as well as `projects` for easier filtering.
## Details
1. The database table is designed as a [closure table](https://www.slideshare.net/slideshow/models-for-hierarchical-data/4179181)
1. When a dependency is transitive then the corresponding `Sbom::Occurrence#ancestors` will contain entries.
1. When a dependency is a direct dependency then the corresponding `Sbom::Occurrence#ancestors` will contain an `{}`.
1. Dependencies can be both direct and transitive.
1. There can be more than one version of a given dependency in a project (for example Node allows that).
1. There can be more than one `Sbom::Occurrence` for a given dependency version, for example in monorepos. These `Sbom::Occurrence` rows should have a different `input_file_path` and `source_id` (however we will not use `source_id` when building the dependency tree to avoid SQL JOIN).

View File

@ -212,6 +212,55 @@ To customize policy enforcement, you can define a policy's scope to either inclu
specified projects, groups, or compliance framework labels. For more details, see
[Scope](_index.md#scope).
## Pipeline execution policy limits
For performance reasons, GitLab limits the number of pipeline execution policies that can run as part of a security policy project or pipeline. By default, these are the maximum number of pipelines execution policies:
- Five per security policy project
- Five per pipeline
### Adjust policy limits
{{< history >}}
- [Configurable limits introduced](https://gitlab.com/groups/gitlab-org/-/epics/16929) in GitLab 17.11.
{{< /history >}}
The *Maximum 5 pipeline execution policies per security policy project* limit can be adjusted at different levels:
#### Adjust the limit for an instance
{{< details >}}
- Offering: GitLab Self-Managed
{{< /details >}}
On GitLab Self-Managed instances, administrators can adjust the limits for the entire instance, up to a maximum of 20 pipeline execution policies:
1. Go to **Admin Area** > **Settings** > **Security and compliance**.
1. Expand the **Security policies** section.
1. Set a new value for **Maximum number of pipeline execution policies allowed per security policy configuration**.
1. Select **Save changes**.
#### Adjust the limit for a top-level group
GitLab instance administrators can modify the limits for top-level groups. These group limits can exceed the configured or default instance limits.
{{< alert type="note" >}}
Increasing these limits can affect system performance, especially for complex policies or when applying many policies simultaneously.
{{< /alert >}}
To adjust the limit for a top-level group:
1. Go to **Admin Area** > **Overview** > **Groups**.
1. In the row of the top-level group you want to modify, select **Edit**.
1. Set a new value for **Maximum number of pipeline execution policies allowed per security policy configuration**.
1. Select **Save changes**.
When the limit for an individual group is set to zero, the system will fall back to using the instance-wide default value. This ensures that groups with a zero limit can still create pipeline execution policies according to the instance default configuration.
## Manage access to the CI/CD configuration
When you enforce pipeline execution policies on a project, users that trigger pipelines must have at least read-only access to the project that contains the policy CI/CD configuration. You can grant access to the project manually or automatically.

View File

@ -673,7 +673,8 @@ Audit event types belong to the following product categories.
| [`user_auditor_status_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136456) | A user is either made an auditor or removed as an auditor | {{< icon name="check-circle" >}} Yes | GitLab [16.6](https://gitlab.com/gitlab-org/gitlab/-/issues/430235) | User |
| [`user_email_address_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2103) | A user updates their email address | {{< icon name="check-circle" >}} Yes | GitLab [10.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/1370) | User |
| [`user_name_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167484) | A user's name is updated | {{< icon name="check-circle" >}} Yes | GitLab [17.5](https://gitlab.com/gitlab-org/gitlab/-/issues/486532) | User |
| [`user_profile_visiblity_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129149) | User toggles private profile user setting | {{< icon name="dotted-circle" >}} No | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129149) | User |
| [`user_profile_visibility_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187207) | User toggles private profile user setting | {{< icon name="dotted-circle" >}} No | GitLab [17.11](https://gitlab.com/gitlab-org/gitlab/-/issues/474386) | User |
| [`user_profile_visiblity_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129149) | User toggles private profile user setting (DEPRECATED). Use `user_profile_visibility_updated` instead. | {{< icon name="dotted-circle" >}} No | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129149) | User |
| [`user_username_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106086) | A user's username is updated | {{< icon name="check-circle" >}} Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369329) | User |
### Value stream management

View File

@ -136,7 +136,7 @@ You can also provide it with additional context.
| Epics | Either:<br>• The URL of the group or epic. <br>• The epic ID and the name of the group the epic is in. | Examples:<br>• List all epics in `https://gitlab.com/groups/namespace/group`<br>• Summarize the epic: `https://gitlab.com/groups/namespace/group/-/epics/42`<br>`Summarize epic 42 in group namespace/group` |
| Issues | Either:<br>• The URL of the project or issue. <br>• The issue ID in the current or another project. | Examples:<br>• List all issues in the project at `https://gitlab.com/namespace/project`<br>• Summarize the issue at `https://gitlab.com/namespace/project/-/issues/103`<br>• Review the comment with ID `42` in `https://gitlab.com/namespace/project/-/issues/103`<br>• List all comments on the issue at `https://gitlab.com/namespace/project/-/issues/103`<br>• Summarize issue `103` in this project |
| Merge requests | Either:<br>• The URL of the merge request. <br>• The merge request ID in the current or another project. |• Summarize `https://gitlab.com/namespace/project/-/merge_requests/103`<br>• Review the diffs in `https://gitlab.com/namespace/project/-/merge_requests/103`<br>• Summarize the comments on `https://gitlab.com/namespace/project/-/merge_requests/103`<br>• Summarize merge request `103` in this project |
| Merge request pipelines | The merge request ID in the current or another project. |• Review the failures in merge request `12345`<br>• Can you identify the cause of the error in the merge request `54321` in project `gitlab-org/gitlab-qa` |
| Merge request pipelines | The merge request ID in the current or another project. |• Review the failures in merge request `12345`<br>• Can you identify the cause of the error in the merge request `54321` in project `gitlab-org/gitlab-qa` <br>• Suggest a solution to the pipeline failure in `https://gitlab.com/namespace/project/-/merge_requests/54321` |
Workflow also has access to the GitLab [Search API](../../api/search.md) to find related issues or merge requests.

View File

@ -7,6 +7,7 @@ module API
expose :title, documentation: { type: 'string', example: 'Sample key 25' }
expose :created_at, documentation: { type: 'dateTime', example: '2015-09-03T07:24:44.627Z' }
expose :expires_at, documentation: { type: 'dateTime', example: '2020-09-03T07:24:44.627Z' }
expose :last_used_at, documentation: { type: 'dateTime', example: '2020-09-03T07:24:44.627Z' }
expose :publishable_key, as: :key, documentation:
{ type: 'string',
example: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6Yjz\

View File

@ -230,29 +230,26 @@ module Gitlab
# If the parent doesn't exist, or the current user can't access it
unless parent && current_user.can?(:read_work_item, parent)
return _("This parent does not exist or you don't have sufficient permission.")
return _("This parent item does not exist or you don't have sufficient permission.")
end
# If the child has already been added to the parent
if child_work_item && child_work_item.work_item_parent == parent
return format(_('%{child_type} %{child_reference} has already been added to parent %{parent_reference}.'),
return format(_('%{child_reference} has already been added to parent %{parent_reference}.'),
child_reference: child_work_item.to_reference,
parent_reference: parent.to_reference,
child_type: child_work_item.work_item_type.name)
parent_reference: parent.to_reference)
end
# If the parent is confidential, but the child is not
if parent.confidential? && !child.confidential?
return format(_("Cannot assign a confidential parent to a non-confidential %{child_type}. Make the " \
"%{child_type} confidential and try again"), child_type: child.work_item_type&.name || 'Issue')
return _("Cannot assign a confidential parent item to a non-confidential child item. Make the child item " \
"confidential and try again.")
end
# Check hierarchy restriction
return unless child_work_item && !hierarchy_relationship_allowed?(parent, child_work_item)
format(_("Cannot assign a child %{child_type} to a %{parent_type}"),
child_type: child_work_item.work_item_type.name,
parent_type: parent.work_item_type.name)
_("Cannot assign this child type to parent type.")
end
def fetch_child_work_item(child)

View File

@ -687,7 +687,7 @@ msgstr[1] ""
msgid "%{chartTitle} no data series"
msgstr ""
msgid "%{child_type} %{child_reference} has already been added to parent %{parent_reference}."
msgid "%{child_reference} has already been added to parent %{parent_reference}."
msgstr ""
msgid "%{codeStart}$%{codeEnd} will be treated as the start of a reference to another variable."
@ -6364,6 +6364,9 @@ msgstr ""
msgid "Amazon Q"
msgstr ""
msgid "Amazon Q connectivity check failed: %{message}"
msgstr ""
msgid "AmazonQ|Active cloud connector token not found."
msgstr ""
@ -11909,13 +11912,13 @@ msgstr ""
msgid "Cancelling Preview"
msgstr ""
msgid "Cannot assign a child %{child_type} to a %{parent_type}"
msgstr ""
msgid "Cannot assign a confidential epic to a non-confidential issue. Make the issue confidential and try again"
msgstr ""
msgid "Cannot assign a confidential parent to a non-confidential %{child_type}. Make the %{child_type} confidential and try again"
msgid "Cannot assign a confidential parent item to a non-confidential child item. Make the child item confidential and try again."
msgstr ""
msgid "Cannot assign this child type to parent type."
msgstr ""
msgid "Cannot be merged automatically"
@ -12350,6 +12353,9 @@ msgstr ""
msgid "Checking group path availability…"
msgstr ""
msgid "Checking if %{message} failed: %{error}"
msgstr ""
msgid "Checking if merge request can be merged…"
msgstr ""
@ -27393,6 +27399,9 @@ msgstr ""
msgid "GitLab container registry API not supported"
msgstr ""
msgid "GitLab credentials used by Amazon Q are valid"
msgstr ""
msgid "GitLab detected an attempt to sign in to your %{host} account using an incorrect verification code"
msgstr ""
@ -27417,6 +27426,9 @@ msgstr ""
msgid "GitLab informs you if a new version is available. %{link_start}What information does GitLab Inc. collect?%{link_end}"
msgstr ""
msgid "GitLab instance is reachable by Amazon Q"
msgstr ""
msgid "GitLab is a complete DevOps platform, delivered as a single application, fundamentally changing the way Development, Security, and Ops teams collaborate"
msgstr ""
@ -35872,6 +35884,9 @@ msgstr ""
msgid "Manage secret detection behavior for all projects in your GitLab instance"
msgstr ""
msgid "Manage security policy settings in your GitLab instance"
msgstr ""
msgid "Manage templates..."
msgstr ""
@ -36292,6 +36307,9 @@ msgstr ""
msgid "Maximum number of Helm packages that can be listed per channel. Must be at least 1."
msgstr ""
msgid "Maximum number of actions per scan execution policy"
msgstr ""
msgid "Maximum number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3). Setting to 0 does not disable throttling."
msgstr ""
@ -36304,6 +36322,9 @@ msgstr ""
msgid "Maximum number of mirrors that can be synchronizing at the same time."
msgstr ""
msgid "Maximum number of pipeline execution policies allowed per security policy configuration"
msgstr ""
msgid "Maximum number of possible retries for Elasticsearch search requests."
msgstr ""
@ -53215,6 +53236,9 @@ msgstr ""
msgid "Security inventory"
msgstr ""
msgid "Security policies"
msgstr ""
msgid "Security policy"
msgstr ""
@ -59986,6 +60010,9 @@ msgstr ""
msgid "The maximum file size is %{size}."
msgstr ""
msgid "The maximum number of pipeline execution policies allowed per security policy configuration can exceed the limit for the instance. Set to 0 to use instance limit."
msgstr ""
msgid "The maximum number of tags that a single worker accepts for cleanup. If the number of tags goes above this limit, the list of tags to delete is truncated to this number. To remove this limit, set it to 0."
msgstr ""
@ -61167,7 +61194,7 @@ msgstr ""
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
msgid "This parent does not exist or you don't have sufficient permission."
msgid "This parent item does not exist or you don't have sufficient permission."
msgstr ""
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}"
@ -63497,6 +63524,9 @@ msgstr ""
msgid "Unknown Error"
msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Unknown format"
msgstr ""

47
scripts/extract_fingerprints Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# Script to extract SQL query fingerprints from auto-explain logs
require 'json'
require 'zlib'
if ARGV.size < 2
puts "Usage: #{$PROGRAM_NAME} <input_file> <output_file>"
exit 1
end
input_file = ARGV[0]
output_file = ARGV[1]
unless File.exist?(input_file)
puts "Error: Input file not found - #{input_file}"
exit 1
end
fingerprints = Set.new
begin
# Handle both compressed and uncompressed files
if input_file.end_with?('.gz')
Zlib::GzipReader.open(input_file) do |gz|
gz.each_line do |line|
data = JSON.parse(line)
fingerprints.add(data['fingerprint']) if data['fingerprint']
rescue JSON::ParserError
# empty
end
end
else
File.foreach(input_file) do |line|
data = JSON.parse(line)
fingerprints.add(data['fingerprint']) if data['fingerprint']
rescue JSON::ParserError
# empty
end
end
File.open(output_file, 'w') { |f| fingerprints.each { |fp| f.puts(fp) } }
rescue StandardError => e
puts "Error: #{e.message}"
exit 1
end

View File

@ -224,3 +224,15 @@ function fixtures_directory_exists() {
function upload_fixtures_package() {
upload_package "${FIXTURES_PACKAGE}" "${FIXTURES_PACKAGE_URL}"
}
# Dump auto-explain logs fingerprints
export FINGERPRINTS_PACKAGE="query-fingerprints.tar.gz"
export FINGERPRINTS_FILE="query_fingerprints.txt"
export FINGERPRINTS_PACKAGE_URL="${API_PACKAGES_BASE_URL}/auto-explain-logs/master/${FINGERPRINTS_PACKAGE}"
function extract_and_upload_fingerprints() {
echo "Extracting SQL query fingerprints from ${RSPEC_AUTO_EXPLAIN_LOG_PATH}"
ruby scripts/extract_fingerprints "${RSPEC_AUTO_EXPLAIN_LOG_PATH}" "${FINGERPRINTS_FILE}"
create_package "${FINGERPRINTS_PACKAGE}" "${FINGERPRINTS_FILE}"
upload_package "${FINGERPRINTS_PACKAGE}" "${FINGERPRINTS_PACKAGE_URL}"
}

View File

@ -461,6 +461,17 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
expect(application_settings.reload.ci_max_includes).to eq(200)
end
end
context 'deletion adjourned period' do
let(:application_settings) { ApplicationSetting.current }
it 'updates deletion_adjourned_period setting' do
put :update, params: { application_setting: { deletion_adjourned_period: 6 } }
expect(response).to redirect_to(general_admin_application_settings_path)
expect(application_settings.reload.deletion_adjourned_period).to eq(6)
end
end
end
describe 'PUT #reset_registration_token', feature_category: :user_management do

View File

@ -28,6 +28,13 @@
],
"format": "date-time"
},
"last_used_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"key": {
"type": "string"
},
@ -59,4 +66,4 @@
}
},
"additionalProperties": false
}
}

View File

@ -0,0 +1,124 @@
import { GlLink } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import FormGroup from '~/admin/application_settings/deletion_protection/components/form_group.vue';
import {
I18N_DELETION_PROTECTION,
DEL_ADJ_PERIOD_MIN_LIMIT_ERROR,
DEL_ADJ_PERIOD_MAX_LIMIT_ERROR,
} from '~/admin/application_settings/deletion_protection/constants';
describe('Form group component', () => {
let wrapper;
const findGlLink = () => wrapper.findComponent(GlLink);
const findDeletionAdjournedPeriodInput = () => wrapper.findByTestId('deletion_adjourned_period');
const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = mountExtended(FormGroup, {
propsData: {
deletionAdjournedPeriod: 7,
...props,
},
provide,
});
};
beforeEach(() => {
createComponent();
});
it('renders an input for setting the deletion adjourned period', () => {
expect(
wrapper.findByLabelText(I18N_DELETION_PROTECTION.label, { exact: false }).attributes(),
).toMatchObject({
name: 'application_setting[deletion_adjourned_period]',
type: 'number',
min: '1',
max: '90',
});
});
it('displays the help text', () => {
expect(wrapper.findByText(I18N_DELETION_PROTECTION.helpText).exists()).toBe(true);
});
it('displays the help link', () => {
expect(findGlLink().text()).toContain(I18N_DELETION_PROTECTION.learnMore);
expect(findGlLink().attributes('href')).toBe(
helpPagePath('administration/settings/visibility_and_access_controls', {
anchor: 'delayed-project-deletion',
}),
);
});
describe.each`
value | errorMessage
${''} | ${DEL_ADJ_PERIOD_MIN_LIMIT_ERROR}
${'91'} | ${DEL_ADJ_PERIOD_MAX_LIMIT_ERROR}
${'-1'} | ${DEL_ADJ_PERIOD_MIN_LIMIT_ERROR}
`('when the input has a value of $value', ({ value, errorMessage }) => {
describe('when input is blured', () => {
it('displays error message', async () => {
findDeletionAdjournedPeriodInput().vm.$emit('input', value);
findDeletionAdjournedPeriodInput().vm.$emit('blur');
await nextTick();
expect(findDeletionAdjournedPeriodInput().attributes('aria-invalid')).toBe('true');
expect(wrapper.findByText(errorMessage).exists()).toBe(true);
});
});
describe('when input emits invalid event', () => {
it('displays error message, prevents default and focuses on input', async () => {
findDeletionAdjournedPeriodInput().vm.$emit('input', value);
const event = {
preventDefault: jest.fn(),
};
const focusSpy = jest.spyOn(findDeletionAdjournedPeriodInput().element, 'focus');
findDeletionAdjournedPeriodInput().vm.$emit('invalid', event);
await nextTick();
expect(findDeletionAdjournedPeriodInput().attributes('aria-invalid')).toBe('true');
expect(wrapper.findByText(errorMessage).exists()).toBe(true);
expect(event.preventDefault).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();
});
});
});
describe('when input has valid value', () => {
describe('when input is blured', () => {
it('does not display error message', async () => {
findDeletionAdjournedPeriodInput().vm.$emit('input', '50');
findDeletionAdjournedPeriodInput().vm.$emit('blur');
await nextTick();
expect(findDeletionAdjournedPeriodInput().attributes('aria-invalid')).toBe(undefined);
expect(wrapper.findByText(DEL_ADJ_PERIOD_MIN_LIMIT_ERROR).exists()).toBe(false);
expect(wrapper.findByText(DEL_ADJ_PERIOD_MAX_LIMIT_ERROR).exists()).toBe(false);
});
});
describe('when input emits invalid event', () => {
it('does not display error message', async () => {
findDeletionAdjournedPeriodInput().vm.$emit('input', '50');
const event = {
preventDefault: jest.fn(),
};
findDeletionAdjournedPeriodInput().vm.$emit('invalid', event);
await nextTick();
expect(findDeletionAdjournedPeriodInput().attributes('aria-invalid')).toBe(undefined);
expect(wrapper.findByText(DEL_ADJ_PERIOD_MIN_LIMIT_ERROR).exists()).toBe(false);
expect(wrapper.findByText(DEL_ADJ_PERIOD_MAX_LIMIT_ERROR).exists()).toBe(false);
});
});
});
});

View File

@ -0,0 +1,56 @@
import { createWrapper } from '@vue/test-utils';
import { initAdminDeletionProtectionSettings } from '~/admin/application_settings/deletion_protection';
import { parseFormProps } from '~/admin/application_settings/deletion_protection/utils';
import FormGroup from '~/admin/application_settings/deletion_protection/components/form_group.vue';
jest.mock('~/admin/application_settings/deletion_protection/utils', () => ({
parseFormProps: jest.fn().mockReturnValue({
deletionAdjournedPeriod: 7,
delayedGroupDeletion: false,
delayedProjectDeletion: false,
}),
}));
describe('initAdminDeletionProtectionSettings', () => {
let appRoot;
let wrapper;
const createAppRoot = () => {
appRoot = document.createElement('div');
appRoot.setAttribute('id', 'js-admin-deletion-protection-settings');
appRoot.dataset.deletionAdjournedPeriod = 7;
appRoot.dataset.delayedGroupDeletion = false;
appRoot.dataset.delayedProjectDeletion = false;
document.body.appendChild(appRoot);
};
afterEach(() => {
if (appRoot) {
appRoot.remove();
appRoot = null;
}
});
const findFormGroup = () => wrapper.findComponent(FormGroup);
describe('when there is no app root', () => {
it('returns false', () => {
expect(initAdminDeletionProtectionSettings()).toBe(false);
});
});
describe('when there is an app root', () => {
beforeEach(() => {
createAppRoot();
wrapper = createWrapper(initAdminDeletionProtectionSettings());
});
it('renders FormGroup', () => {
expect(findFormGroup().exists()).toBe(true);
});
it('parses the form props from the dataset', () => {
expect(parseFormProps).toHaveBeenCalledWith(appRoot.dataset);
});
});
});

View File

@ -0,0 +1,26 @@
import { parseFormProps } from '~/admin/application_settings/deletion_protection/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
describe('deletion protection utils', () => {
describe('parseFormProps', () => {
const input = {
deletionAdjournedPeriod: '7',
delayedGroupDeletion: 'true',
delayedProjectDeletion: 'false',
};
it('returns the expected result', () => {
expect(parseFormProps(input)).toStrictEqual({
deletionAdjournedPeriod: parseInt(input.deletionAdjournedPeriod, 10),
delayedGroupDeletion: parseBoolean(input.delayedGroupDeletion),
delayedProjectDeletion: parseBoolean(input.delayedProjectDeletion),
});
});
it('does not attempt to parse an undefined adjourned period', () => {
expect(parseFormProps({ deletionAdjournedPeriod: undefined })).toMatchObject({
deletionAdjournedPeriod: undefined,
});
});
});
});

View File

@ -28,7 +28,7 @@ describe('MetricPopover', () => {
};
const findMetricLabel = () => wrapper.findByTestId('metric-label');
const findMetricLink = () => wrapper.find('[data-testid="metric-link"]');
const findMetricLink = () => wrapper.findByTestId('metric-link');
const findMetricDescription = () => wrapper.findByTestId('metric-description');
const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);

View File

@ -1,8 +1,8 @@
import { GlLoadingIcon, GlTable, GlButton } from '@gitlab/ui';
import { getAllByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Papa from 'papaparse';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CsvViewer from '~/blob/csv/csv_viewer.vue';
import PapaParseAlert from '~/blob/components/papa_parse_alert.vue';
import { MAX_ROWS_TO_RENDER } from '~/blob/csv/constants';
@ -16,7 +16,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
const createComponent = ({
csv = validCsv,
remoteFile = false,
mountFunction = shallowMount,
mountFunction = shallowMountExtended,
} = {}) => {
wrapper = mountFunction(CsvViewer, {
propsData: {
@ -30,7 +30,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(PapaParseAlert);
const findSwitchToRawViewBtn = () => wrapper.findComponent(GlButton);
const findLargeCsvText = () => wrapper.find('[data-testid="large-csv-text"]');
const findLargeCsvText = () => wrapper.findByTestId('large-csv-text');
it('should render loading spinner', () => {
createComponent();
@ -70,7 +70,7 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
});
it('renders the CSV table with the correct content', async () => {
createComponent({ mountFunction: mount });
createComponent({ mountFunction: mountExtended });
await nextTick();
expect(getAllByRole(wrapper.element, 'row', { name: /One/i })).toHaveLength(1);

View File

@ -49,6 +49,9 @@ describe('stage column component', () => {
const findStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
const findStageColumnGroup = () => wrapper.find('[data-testid="stage-column-group"]');
const findAllStageColumnGroups = () => wrapper.findAll('[data-testid="stage-column-group"]');
const findAllStageColumnFailedTitle = () => wrapper.find('[data-testid="failed-jobs-title"]');
const findAllStageColumnFailedGroups = () =>
wrapper.findAll('[data-testid="stage-column-group-failed"]');
const findJobItem = () => wrapper.findComponent(JobItem);
const findActionComponent = () => wrapper.findComponent(ActionComponent);
@ -106,6 +109,39 @@ describe('stage column component', () => {
});
});
describe('when has failed jobs', () => {
beforeEach(() => {
createComponent({
method: shallowMount,
props: {
groups: [
{
jobs: [mockJob],
name: 'test2',
size: 1,
title: 'Bird',
status: {
group: 'failed',
},
},
{
jobs: [mockJob],
name: 'test',
size: 1,
title: 'Fish',
},
],
},
});
});
it('shows failed jobs grouped', () => {
expect(findAllStageColumnFailedGroups().length).toBe(1);
expect(findAllStageColumnFailedTitle().text()).toEqual('Failed jobs');
expect(findAllStageColumnGroups().length).toBe(1);
});
});
describe('job', () => {
describe('text handling', () => {
beforeEach(() => {

View File

@ -342,7 +342,7 @@ describe('Pipeline graph wrapper', () => {
await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(findStageColumnTitle().text()).toBe('');
expect(findStageColumnTitle().exists()).toBe(false);
});
it('saves the view type to local storage', async () => {

View File

@ -1,6 +1,6 @@
import { GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_confirmation.vue';
@ -9,7 +9,7 @@ describe('Remove cluster confirmation modal', () => {
const showMock = jest.fn();
const createComponent = ({ props = {}, stubs = {} } = {}) => {
wrapper = mount(RemoveClusterConfirmation, {
wrapper = mountExtended(RemoveClusterConfirmation, {
propsData: {
clusterPath: 'clusterPath',
clusterName: 'clusterName',
@ -26,10 +26,9 @@ describe('Remove cluster confirmation modal', () => {
describe('two buttons', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findRemoveIntegrationButton = () =>
wrapper.find('[data-testid="remove-integration-button"]');
const findRemoveIntegrationButton = () => wrapper.findByTestId('remove-integration-button');
const findRemoveIntegrationAndResourcesButton = () =>
wrapper.find('[data-testid="remove-integration-and-resources-button"]');
wrapper.findByTestId('remove-integration-and-resources-button');
beforeEach(() => {
createComponent({

View File

@ -1,7 +1,6 @@
import { GlLoadingIcon, GlTable, GlAvatar, GlEmptyState } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
@ -57,50 +56,48 @@ describe('Incidents List', () => {
const findIncidentLink = () => wrapper.findByTestId('incident-link');
function mountComponent({ data = {}, loading = false, provide = {} } = {}) {
wrapper = extendedWrapper(
mount(IncidentsList, {
data() {
return {
incidents: [],
incidentsCount: {},
...data,
};
},
mocks: {
$apollo: {
queries: {
incidents: {
loading,
},
wrapper = mountExtended(IncidentsList, {
data() {
return {
incidents: [],
incidentsCount: {},
...data,
};
},
mocks: {
$apollo: {
queries: {
incidents: {
loading,
},
},
},
provide: {
projectPath: '/project/path',
newIssuePath,
incidentTemplateName,
incidentType,
issuePath: '/project/issues',
publishedAvailable: true,
emptyListSvgPath,
textQuery: '',
authorUsernameQuery: '',
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
canCreateIncident: true,
...provide,
},
stubs: {
GlButton: true,
GlAvatar: true,
GlEmptyState: true,
ServiceLevelAgreementCell: true,
PaginatedTableWithSearchAndTabs: stubComponent(PaginatedTableWithSearchAndTabs, {
template: RENDER_ALL_SLOTS_TEMPLATE,
}),
},
}),
);
},
provide: {
projectPath: '/project/path',
newIssuePath,
incidentTemplateName,
incidentType,
issuePath: '/project/issues',
publishedAvailable: true,
emptyListSvgPath,
textQuery: '',
authorUsernameQuery: '',
assigneeUsernameQuery: '',
slaFeatureAvailable: true,
canCreateIncident: true,
...provide,
},
stubs: {
GlButton: true,
GlAvatar: true,
GlEmptyState: true,
ServiceLevelAgreementCell: true,
PaginatedTableWithSearchAndTabs: stubComponent(PaginatedTableWithSearchAndTabs, {
template: RENDER_ALL_SLOTS_TEMPLATE,
}),
},
});
}
it('shows the loading state', () => {

View File

@ -1,5 +1,5 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import { STATUS_CLOSED } from '~/issues/constants';
import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
@ -52,9 +52,10 @@ describe('CE IssueCardTimeInfo component', () => {
const findMilestone = () => wrapper.findComponent(IssuableMilestone);
const findWorkItemAttribute = () => wrapper.findComponent(WorkItemAttribute);
const findDueDateIcon = () => wrapper.findByTestId('issuable-due-date').findComponent(GlIcon);
const mountComponent = ({ issue = issueObject() } = {}) =>
shallowMount(IssueCardTimeInfo, {
shallowMountExtended(IssueCardTimeInfo, {
propsData: { issue },
stubs: {
WorkItemAttribute,
@ -82,8 +83,7 @@ describe('CE IssueCardTimeInfo component', () => {
wrapper = mountComponent({ issue: object({ dueDate: '2020-12-12' }) });
expect(findWorkItemAttribute().props('title')).toBe('Dec 12, 2020');
expect(findWorkItemAttribute().props('tooltipText')).toBe('Due date');
const datesEl = wrapper.find('[data-testid="issuable-due-date"]');
expect(datesEl.findComponent(GlIcon).props()).toMatchObject({
expect(findDueDateIcon().props()).toMatchObject({
variant: 'current',
name: 'calendar',
});
@ -94,8 +94,7 @@ describe('CE IssueCardTimeInfo component', () => {
describe('when issue is open', () => {
it('renders in red with overdue icon', () => {
wrapper = mountComponent({ issue: object({ dueDate: '2020-10-10' }) });
const datesEl = wrapper.find('[data-testid="issuable-due-date"]');
expect(datesEl.findComponent(GlIcon).props()).toMatchObject({
expect(findDueDateIcon().props()).toMatchObject({
variant: 'danger',
name: 'calendar-overdue',
});
@ -108,8 +107,7 @@ describe('CE IssueCardTimeInfo component', () => {
issue: object({ dueDate: '2020-10-10', state: STATUS_CLOSED }),
});
const datesEl = wrapper.find('[data-testid="issuable-due-date"]');
expect(datesEl.findComponent(GlIcon).props()).toMatchObject({
expect(findDueDateIcon().props()).toMatchObject({
variant: 'current',
name: 'calendar',
});
@ -143,7 +141,7 @@ describe('CE IssueCardTimeInfo component', () => {
it('renders time estimate', () => {
wrapper = mountComponent();
const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
const timeEstimate = wrapper.findByTestId('time-estimate');
expect(findWorkItemAttribute().props('title')).toBe('1w');
expect(findWorkItemAttribute().props('tooltipText')).toBe('Estimate');

View File

@ -6,7 +6,6 @@ import {
GlFormRadio,
GlSprintf,
} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase, merge } from 'lodash';
@ -16,6 +15,7 @@ import { createAlert } from '~/alert';
import * as urlUtility from '~/lib/utils/url_utility';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
@ -119,8 +119,8 @@ describe('ForkForm component', () => {
});
};
const createComponent = createComponentFactory(shallowMount);
const createFullComponent = createComponentFactory(mount);
const createComponent = createComponentFactory(shallowMountExtended);
const createFullComponent = createComponentFactory(mountExtended);
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
@ -133,23 +133,21 @@ describe('ForkForm component', () => {
axiosMock.restore();
});
const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]');
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]');
const findPrivateRadio = () => wrapper.findByTestId('radio-private');
const findInternalRadio = () => wrapper.findByTestId('radio-internal');
const findPublicRadio = () => wrapper.findByTestId('radio-public');
const findForkNameInput = () => wrapper.findByTestId('fork-name-input');
const findForkUrlInput = () => wrapper.findComponent(ProjectNamespace);
const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]');
const findForkDescriptionTextarea = () =>
wrapper.find('[data-testid="fork-description-textarea"]');
const findVisibilityRadioGroup = () =>
wrapper.find('[data-testid="fork-visibility-radio-group"]');
const findBranchesRadioGroup = () => wrapper.find('[data-testid="fork-branches-radio-group"]');
const findForkSlugInput = () => wrapper.findByTestId('fork-slug-input');
const findForkDescriptionTextarea = () => wrapper.findByTestId('fork-description-textarea');
const findVisibilityRadioGroup = () => wrapper.findByTestId('fork-visibility-radio-group');
const findBranchesRadioGroup = () => wrapper.findByTestId('fork-branches-radio-group');
it('will go to cancelPath when click cancel button', () => {
createComponent();
const { cancelPath } = DEFAULT_PROVIDE;
const cancelButton = wrapper.find('[data-testid="cancel-button"]');
const cancelButton = wrapper.findByTestId('cancel-button');
expect(cancelButton.attributes('href')).toBe(cancelPath);
});

View File

@ -1,9 +1,9 @@
import { GlAlert, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@ -26,10 +26,10 @@ describe('Code Coverage', () => {
const findListBoxItems = () => wrapper.findAllComponents(GlListboxItem);
const findFirstListBoxItem = () => findListBoxItems().at(0);
const findSecondListBoxItem = () => findListBoxItems().at(1);
const findDownloadButton = () => wrapper.find('[data-testid="download-button"]');
const findDownloadButton = () => wrapper.findByTestId('download-button');
const createComponent = () => {
wrapper = shallowMount(CodeCoverage, {
wrapper = shallowMountExtended(CodeCoverage, {
propsData: {
graphEndpoint,
graphStartDate,

View File

@ -131,8 +131,7 @@ describe('Settings Panel', () => {
findContainerRegistrySettings().findComponent(GlSprintf);
const findContainerRegistryAccessLevelInput = () =>
wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]');
const findPackageAccessLevel = () =>
wrapper.find('[data-testid="package-registry-access-level"]');
const findPackageAccessLevel = () => wrapper.findByTestId('package-registry-access-level');
const findPackageRegistryEnabledInput = () => wrapper.find('[name="package_registry_enabled"]');
const findPackageRegistryAccessLevelHiddenInput = () =>
wrapper.find(

View File

@ -1,7 +1,7 @@
import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
@ -18,7 +18,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
let mock;
function buildWrapper(propsData = {}) {
wrapper = shallowMount(WikiContent, {
wrapper = shallowMountExtended(WikiContent, {
provide: {
contentApi: PATH,
},
@ -36,7 +36,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findContent = () => wrapper.find('[data-testid="wiki-page-content"]');
const findContent = () => wrapper.findByTestId('wiki-page-content');
describe('when loading content', () => {
beforeEach(() => {

View File

@ -1,5 +1,5 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/vue_shared/components/registry/metadata_item.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
@ -11,7 +11,7 @@ describe('Metadata Item', () => {
};
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
wrapper = shallowMountExtended(component, {
propsData,
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
@ -21,9 +21,9 @@ describe('Metadata Item', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = (w = wrapper) => w.findComponent(GlLink);
const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
const findText = () => wrapper.findByTestId('metadata-item-text');
const findTooltipOnTruncate = (w = wrapper) => w.findComponent(TooltipOnTruncate);
const findTextTooltip = () => wrapper.find('[data-testid="text-tooltip-container"]');
const findTextTooltip = () => wrapper.findByTestId('text-tooltip-container');
const SIZE_TO_TAILWIND_UTILITY_MAPPING = {
s: 'gl-max-w-20',

View File

@ -40,13 +40,13 @@ describe('CreateWorkItemModal', () => {
const createComponent = ({
asDropdownItem = false,
hideButton = false,
workItemTypeName = 'EPIC',
preselectedWorkItemType = 'EPIC',
relatedItem = null,
alwaysShowWorkItemTypeSelect = false,
} = {}) => {
wrapper = shallowMount(CreateWorkItemModal, {
propsData: {
workItemTypeName,
preselectedWorkItemType,
asDropdownItem,
hideButton,
relatedItem,
@ -111,8 +111,8 @@ describe('CreateWorkItemModal', () => {
expect(findTrigger().exists()).toBe(false);
});
it('has text of "New item" when the `alwaysShowWorkItemTypeSelect` prop is `true` and we also have a `workItemTypeName`', () => {
createComponent({ alwaysShowWorkItemTypeSelect: true, workItemTypeName: 'ISSUE' });
it('has text of "New item" when the `alwaysShowWorkItemTypeSelect` prop is `true` and we also have a `preselectedWorkItemType`', () => {
createComponent({ alwaysShowWorkItemTypeSelect: true, preselectedWorkItemType: 'ISSUE' });
expect(findTrigger().text()).toBe('New item');
});
@ -148,9 +148,9 @@ describe('CreateWorkItemModal', () => {
expect(findCreateModal().props('visible')).toBe(false);
});
for (const [workItemTypeName, vals] of Object.entries(WORK_ITEMS_TYPE_MAP)) {
it(`has link to new work item page in modal header for ${workItemTypeName}`, async () => {
createComponent({ workItemTypeName });
for (const [preselectedWorkItemType, vals] of Object.entries(WORK_ITEMS_TYPE_MAP)) {
it(`has link to new work item page in modal header for ${preselectedWorkItemType}`, async () => {
createComponent({ preselectedWorkItemType });
const routeParamName = vals.routeParamName || WORK_ITEM_TYPE_ROUTE_WORK_ITEM;

View File

@ -257,7 +257,7 @@ and even more`,
title:
'item 2 with a really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really really rea',
visible: true,
workItemTypeName: 'TASK',
preselectedWorkItemType: 'TASK',
});
findCreateWorkItemModal().vm.$emit('workItemCreated');
@ -295,7 +295,7 @@ and even more`,
showProjectSelector: true,
title: 'item 1',
visible: true,
workItemTypeName: 'ISSUE',
preselectedWorkItemType: 'ISSUE',
});
});
});
@ -321,7 +321,7 @@ and even more`,
showProjectSelector: false,
title: 'item 1',
visible: true,
workItemTypeName: 'TASK',
preselectedWorkItemType: 'TASK',
});
});
});

View File

@ -449,6 +449,20 @@ RSpec.describe ApplicationSettingsHelper, feature_category: :shared do
end
end
describe '.deletion_protection_data' do
let_it_be(:application_setting) { build(:application_setting) }
before do
application_setting.deletion_adjourned_period = 1
helper.instance_variable_set(:@application_setting, application_setting)
end
subject { helper.deletion_protection_data }
it { is_expected.to eq({ deletion_adjourned_period: 1 }) }
end
describe '#vscode_extension_marketplace_settings_view' do
let(:feature_flag) { true }
let(:application_setting) { build(:application_setting) }

View File

@ -15,6 +15,7 @@ RSpec.describe API::Entities::SSHKey, feature_category: :system_access do
title: key.title,
created_at: key.created_at,
expires_at: key.expires_at,
last_used_at: key.last_used_at,
key: key.publishable_key,
usage_type: 'auth_and_signing'
)

View File

@ -3353,7 +3353,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :text_editors d
_, updates, message = service.execute(content, task_work_item)
expect(updates).to be_empty
expect(message).to eq("This parent does not exist or you don't have sufficient permission.")
expect(message).to eq("This parent item does not exist or you don't have sufficient permission.")
expect(task_work_item.reload.work_item_parent).to be_nil
end
end
@ -3367,7 +3367,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :text_editors d
_, updates, message = service.execute(content, task_work_item_with_parent)
expect(updates).to be_empty
expect(message).to eq("Task #{task_work_item_with_parent.to_reference} has already been added to " \
expect(message).to eq("#{task_work_item_with_parent.to_reference} has already been added to " \
"parent #{parent.to_reference}.")
expect(task_work_item_with_parent.reload.work_item_parent).to eq parent
end
@ -3381,8 +3381,8 @@ RSpec.describe QuickActions::InterpretService, feature_category: :text_editors d
_, updates, message = service.execute(content, task_work_item)
expect(updates).to be_empty
expect(message).to eq("Cannot assign a confidential parent to a non-confidential Task. Make the " \
"Task confidential and try again")
expect(message).to eq("Cannot assign a confidential parent item to a non-confidential child item. Make " \
"the child item confidential and try again.")
expect(task_work_item.reload.work_item_parent).to be_nil
end
end
@ -3395,7 +3395,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :text_editors d
_, updates, message = service.execute(content, task_work_item)
expect(updates).to be_empty
expect(message).to eq("Cannot assign a child Task to a Task")
expect(message).to eq("Cannot assign this child type to parent type.")
expect(task_work_item.reload.work_item_parent).to be_nil
end
end
@ -3408,7 +3408,7 @@ RSpec.describe QuickActions::InterpretService, feature_category: :text_editors d
_, updates, message = service.execute(content, task_work_item)
expect(updates).to be_empty
expect(message).to eq("This parent does not exist or you don't have sufficient permission.")
expect(message).to eq("This parent item does not exist or you don't have sufficient permission.")
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'admin/application_settings/_deletion_protection_settings', feature_category: :system_access do
let_it_be(:application_setting) do
build(
:application_setting,
deletion_adjourned_period: 1
)
end
before do
assign(:application_setting, application_setting)
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(downtier_delayed_deletion: true)
end
it 'renders the deletion protection settings app root' do
render
expect(rendered).to have_selector('#js-admin-deletion-protection-settings')
end
end
end