Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-15 18:12:46 +00:00
parent 4441a8c8e4
commit c638142c8c
105 changed files with 1468 additions and 233 deletions

View File

@ -331,7 +331,6 @@ Layout/LineLength:
- 'app/models/integrations/asana.rb' - 'app/models/integrations/asana.rb'
- 'app/models/integrations/base_chat_notification.rb' - 'app/models/integrations/base_chat_notification.rb'
- 'app/models/integrations/base_issue_tracker.rb' - 'app/models/integrations/base_issue_tracker.rb'
- 'app/models/integrations/bugzilla.rb'
- 'app/models/integrations/chat_message/merge_message.rb' - 'app/models/integrations/chat_message/merge_message.rb'
- 'app/models/integrations/chat_message/note_message.rb' - 'app/models/integrations/chat_message/note_message.rb'
- 'app/models/integrations/chat_message/pipeline_message.rb' - 'app/models/integrations/chat_message/pipeline_message.rb'

View File

@ -4452,7 +4452,6 @@ RSpec/FeatureCategory:
- 'spec/models/integrations/base_issue_tracker_spec.rb' - 'spec/models/integrations/base_issue_tracker_spec.rb'
- 'spec/models/integrations/base_slack_notification_spec.rb' - 'spec/models/integrations/base_slack_notification_spec.rb'
- 'spec/models/integrations/base_third_party_wiki_spec.rb' - 'spec/models/integrations/base_third_party_wiki_spec.rb'
- 'spec/models/integrations/bugzilla_spec.rb'
- 'spec/models/integrations/buildkite_spec.rb' - 'spec/models/integrations/buildkite_spec.rb'
- 'spec/models/integrations/chat_message/alert_message_spec.rb' - 'spec/models/integrations/chat_message/alert_message_spec.rb'
- 'spec/models/integrations/chat_message/base_message_spec.rb' - 'spec/models/integrations/chat_message/base_message_spec.rb'

View File

@ -77,7 +77,6 @@ Style/FormatString:
- 'app/models/diff_note.rb' - 'app/models/diff_note.rb'
- 'app/models/diff_viewer/base.rb' - 'app/models/diff_viewer/base.rb'
- 'app/models/integrations/asana.rb' - 'app/models/integrations/asana.rb'
- 'app/models/integrations/bugzilla.rb'
- 'app/models/integrations/chat_message/pipeline_message.rb' - 'app/models/integrations/chat_message/pipeline_message.rb'
- 'app/models/integrations/confluence.rb' - 'app/models/integrations/confluence.rb'
- 'app/models/integrations/custom_issue_tracker.rb' - 'app/models/integrations/custom_issue_tracker.rb'

View File

@ -3,6 +3,7 @@ import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils'; import { buildApiUrl } from './api_utils';
const PROJECTS_PATH = '/api/:version/projects.json'; const PROJECTS_PATH = '/api/:version/projects.json';
const PROJECT_PATH = '/api/:version/projects/:id';
const PROJECT_MEMBERS_PATH = '/api/:version/projects/:id/members'; const PROJECT_MEMBERS_PATH = '/api/:version/projects/:id/members';
const PROJECT_ALL_MEMBERS_PATH = '/api/:version/projects/:id/members/all'; const PROJECT_ALL_MEMBERS_PATH = '/api/:version/projects/:id/members/all';
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id'; const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
@ -43,6 +44,12 @@ export function createProject(projectData) {
}); });
} }
export function deleteProject(projectId) {
const url = buildApiUrl(PROJECT_PATH).replace(':id', projectId);
return axios.delete(url);
}
export function importProjectMembers(sourceId, targetId) { export function importProjectMembers(sourceId, targetId) {
const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH) const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH)
.replace(':id', sourceId) .replace(':id', sourceId)

View File

@ -1,5 +1,6 @@
<script> <script>
import { GlBadge, GlTruncate } from '@gitlab/ui'; import { GlBadge, GlTruncate } from '@gitlab/ui';
import { stringify } from 'yaml';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { WORKLOAD_STATUS_BADGE_VARIANTS } from '../constants'; import { WORKLOAD_STATUS_BADGE_VARIANTS } from '../constants';
import WorkloadDetailsItem from './workload_details_item.vue'; import WorkloadDetailsItem from './workload_details_item.vue';
@ -26,6 +27,18 @@ export default {
const { annotations } = this.item; const { annotations } = this.item;
return Object.entries(annotations).map(this.getAnnotationsText); return Object.entries(annotations).map(this.getAnnotationsText);
}, },
specYaml() {
return this.getYamlStringFromJSON(this.item.spec);
},
statusYaml() {
return this.getYamlStringFromJSON(this.item.fullStatus);
},
annotationsYaml() {
return this.getYamlStringFromJSON(this.item.annotations);
},
hasFullStatus() {
return Boolean(this.item.fullStatus);
},
}, },
methods: { methods: {
getLabelBadgeText([key, value]) { getLabelBadgeText([key, value]) {
@ -35,6 +48,12 @@ export default {
getAnnotationsText([key, value]) { getAnnotationsText([key, value]) {
return `${key}: ${value}`; return `${key}: ${value}`;
}, },
getYamlStringFromJSON(json) {
if (!json) {
return '';
}
return stringify(json);
},
}, },
i18n: { i18n: {
name: s__('KubernetesDashboard|Name'), name: s__('KubernetesDashboard|Name'),
@ -42,6 +61,7 @@ export default {
labels: s__('KubernetesDashboard|Labels'), labels: s__('KubernetesDashboard|Labels'),
status: s__('KubernetesDashboard|Status'), status: s__('KubernetesDashboard|Status'),
annotations: s__('KubernetesDashboard|Annotations'), annotations: s__('KubernetesDashboard|Annotations'),
spec: s__('KubernetesDashboard|Spec'),
}, },
WORKLOAD_STATUS_BADGE_VARIANTS, WORKLOAD_STATUS_BADGE_VARIANTS,
}; };
@ -62,19 +82,29 @@ export default {
</gl-badge> </gl-badge>
</div> </div>
</workload-details-item> </workload-details-item>
<workload-details-item v-if="item.status" :label="$options.i18n.status"> <workload-details-item v-if="item.status && !item.fullStatus" :label="$options.i18n.status">
<gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{ <gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]" size="sm">{{
item.status item.status
}}</gl-badge></workload-details-item }}</gl-badge>
</workload-details-item>
<workload-details-item v-if="item.fullStatus" :label="$options.i18n.status" collapsible>
<template v-if="item.status" #label>
<span class="gl-mr-2 gl-font-weight-bold">{{ $options.i18n.status }}</span>
<gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]" size="sm">{{
item.status
}}</gl-badge>
</template>
<pre>{{ statusYaml }}</pre>
</workload-details-item>
<workload-details-item
v-if="itemAnnotations.length"
:label="$options.i18n.annotations"
collapsible
> >
<workload-details-item v-if="itemAnnotations.length" :label="$options.i18n.annotations"> <pre>{{ annotationsYaml }}</pre>
<p </workload-details-item>
v-for="annotation of itemAnnotations" <workload-details-item v-if="item.spec" :label="$options.i18n.spec" collapsible>
:key="annotation" <pre>{{ specYaml }}</pre>
class="gl-mb-2 gl-overflow-wrap-anywhere"
>
{{ annotation }}
</p>
</workload-details-item> </workload-details-item>
</ul> </ul>
</template> </template>

View File

@ -1,18 +1,77 @@
<script> <script>
import { GlCollapse, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default { export default {
components: {
GlCollapse,
GlButton,
},
props: { props: {
label: { label: {
type: String, type: String,
required: true, required: true,
}, },
collapsible: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isVisible: false,
};
},
computed: {
chevronIcon() {
return this.isVisible ? 'chevron-down' : 'chevron-right';
},
collapsibleLabel() {
return this.isVisible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
},
methods: {
toggleCollapse() {
this.isVisible = !this.isVisible;
},
},
i18n: {
collapse: __('Collapse'),
expand: __('Expand'),
}, },
}; };
</script> </script>
<template> <template>
<li class="gl-line-height-20 gl-py-3 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"> <li class="gl-line-height-20 gl-py-3 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100">
<label class="gl-font-weight-bold gl-mb-2"> {{ label }} </label> <div
<div class="gl-text-gray-500 gl-mb-0"> :class="{
'gl-display-flex gl-flex-wrap gl-justify-content-space-between gl-align-items-center': collapsible,
}"
>
<slot name="label">
<label class="gl-font-weight-bold gl-mb-0"> {{ label }} </label>
</slot>
<gl-button
v-if="collapsible"
:icon="chevronIcon"
:aria-label="collapsibleLabel"
category="tertiary"
size="small"
class="gl-ml-auto"
@click="toggleCollapse"
/>
</div>
<gl-collapse v-if="collapsible" :visible="isVisible">
<div v-if="isVisible" class="gl-mt-4">
<slot></slot>
</div>
</gl-collapse>
<div v-else class="gl-text-gray-500 gl-mb-0 gl-mt-2">
<slot></slot> <slot></slot>
</div> </div>
</li> </li>

View File

@ -29,6 +29,7 @@ export const apolloProvider = () => {
query: k8sPodsQuery, query: k8sPodsQuery,
data: { data: {
metadata, metadata,
spec: {},
status: { status: {
phase: null, phase: null,
}, },

View File

@ -20,9 +20,9 @@ export const mapWorkloadItem = (item) => {
annotations: item.metadata?.annotations || {}, annotations: item.metadata?.annotations || {},
labels: item.metadata?.labels || {}, labels: item.metadata?.labels || {},
}; };
return { status: item.status, metadata }; return { status: item.status, spec: item.spec, metadata };
} }
return { status: item.status }; return { status: item.status, spec: item.spec };
}; };
export const mapSetItem = (item) => { export const mapSetItem = (item) => {

View File

@ -7,8 +7,7 @@ query getK8sDashboardPods($configuration: LocalConfiguration) {
labels labels
annotations annotations
} }
status { status
phase spec
}
} }
} }

View File

@ -35,6 +35,8 @@ export default {
labels: pod.metadata?.labels, labels: pod.metadata?.labels,
annotations: pod.metadata?.annotations, annotations: pod.metadata?.annotations,
kind: s__('KubernetesDashboard|Pod'), kind: s__('KubernetesDashboard|Pod'),
spec: pod.spec,
fullStatus: pod.status,
}; };
}) || [] }) || []
); );

View File

@ -63,6 +63,9 @@ export const organizationProjects = [
forkingAccessLevel: { forkingAccessLevel: {
stringValue: 'ENABLED', stringValue: 'ENABLED',
}, },
userPermissions: {
removeProject: true,
},
}, },
{ {
id: 'gid://gitlab/Project/7', id: 'gid://gitlab/Project/7',
@ -86,6 +89,9 @@ export const organizationProjects = [
forkingAccessLevel: { forkingAccessLevel: {
stringValue: 'ENABLED', stringValue: 'ENABLED',
}, },
userPermissions: {
removeProject: true,
},
}, },
{ {
id: 'gid://gitlab/Project/6', id: 'gid://gitlab/Project/6',
@ -109,6 +115,9 @@ export const organizationProjects = [
forkingAccessLevel: { forkingAccessLevel: {
stringValue: 'ENABLED', stringValue: 'ENABLED',
}, },
userPermissions: {
removeProject: true,
},
}, },
{ {
id: 'gid://gitlab/Project/5', id: 'gid://gitlab/Project/5',
@ -132,6 +141,9 @@ export const organizationProjects = [
forkingAccessLevel: { forkingAccessLevel: {
stringValue: 'ENABLED', stringValue: 'ENABLED',
}, },
userPermissions: {
removeProject: true,
},
}, },
{ {
id: 'gid://gitlab/Project/1', id: 'gid://gitlab/Project/1',
@ -155,6 +167,9 @@ export const organizationProjects = [
forkingAccessLevel: { forkingAccessLevel: {
stringValue: 'ENABLED', stringValue: 'ENABLED',
}, },
userPermissions: {
removeProject: false,
},
}, },
]; ];

View File

@ -2,7 +2,9 @@
import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { DEFAULT_PER_PAGE } from '~/api'; import { DEFAULT_PER_PAGE } from '~/api';
import { deleteProject } from '~/rest_api';
import { createAlert } from '~/alert'; import { createAlert } from '~/alert';
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants'; import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import projectsQuery from '../graphql/queries/projects.query.graphql'; import projectsQuery from '../graphql/queries/projects.query.graphql';
@ -14,6 +16,9 @@ export default {
errorMessage: s__( errorMessage: s__(
'Organization|An error occurred loading the projects. Please refresh the page to try again.', 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
), ),
deleteErrorMessage: s__(
'Organization|An error occurred deleting the project. Please refresh the page to try again.',
),
emptyState: { emptyState: {
title: s__("Organization|You don't have any projects yet."), title: s__("Organization|You don't have any projects yet."),
description: s__( description: s__(
@ -163,6 +168,20 @@ export default {
startCursor, startCursor,
}); });
}, },
setProjectIsDeleting(project, val) {
this.$set(project.actionLoadingStates, ACTION_DELETE, val);
},
async deleteProject(data) {
try {
this.setProjectIsDeleting(data, true);
await deleteProject(data.id);
} catch (error) {
createAlert({ message: this.$options.i18n.deleteErrorMessage, error, captureError: true });
} finally {
this.setProjectIsDeleting(data, false);
this.$apollo.queries.projects.refetch();
}
},
}, },
}; };
</script> </script>
@ -170,7 +189,12 @@ export default {
<template> <template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
<div v-else-if="nodes.length"> <div v-else-if="nodes.length">
<projects-list :projects="nodes" show-project-icon :list-item-class="listItemClass" /> <projects-list
:projects="nodes"
show-project-icon
:list-item-class="listItemClass"
@delete="deleteProject"
/>
<div v-if="pageInfo.hasNextPage || pageInfo.hasPreviousPage" class="gl-text-center gl-mt-5"> <div v-if="pageInfo.hasNextPage || pageInfo.hasPreviousPage" class="gl-text-center gl-mt-5">
<gl-keyset-pagination <gl-keyset-pagination
v-bind="pageInfo" v-bind="pageInfo"

View File

@ -31,6 +31,9 @@ query getOrganizationProjects(
forkingAccessLevel { forkingAccessLevel {
stringValue stringValue
} }
userPermissions {
removeProject
}
} }
pageInfo { pageInfo {
...PageInfo ...PageInfo

View File

@ -2,6 +2,16 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants'; import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from './constants'; import { QUERY_PARAM_END_CURSOR, QUERY_PARAM_START_CURSOR } from './constants';
const availableProjectActions = (userPermissions) => {
const baseActions = [ACTION_EDIT];
if (userPermissions.removeProject) {
return [...baseActions, ACTION_DELETE];
}
return baseActions;
};
export const formatProjects = (projects) => export const formatProjects = (projects) =>
projects.map( projects.map(
({ ({
@ -11,6 +21,7 @@ export const formatProjects = (projects) =>
issuesAccessLevel, issuesAccessLevel,
forkingAccessLevel, forkingAccessLevel,
webUrl, webUrl,
userPermissions,
...project ...project
}) => ({ }) => ({
...project, ...project,
@ -22,7 +33,10 @@ export const formatProjects = (projects) =>
webUrl, webUrl,
isForked: false, isForked: false,
editPath: `${webUrl}/edit`, editPath: `${webUrl}/edit`,
availableActions: [ACTION_EDIT, ACTION_DELETE], availableActions: availableProjectActions(userPermissions),
actionLoadingStates: {
[ACTION_DELETE]: false,
},
}), }),
); );

View File

@ -36,6 +36,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
confirmLoading: {
type: Boolean,
required: false,
default: false,
},
issuesCount: { issuesCount: {
type: [Number, String], type: [Number, String],
required: false, required: false,
@ -74,6 +79,7 @@ export default {
attributes: { attributes: {
variant: 'danger', variant: 'danger',
disabled: this.confirmDisabled, disabled: this.confirmDisabled,
loading: this.confirmLoading,
'data-testid': 'confirm-delete-button', 'data-testid': 'confirm-delete-button',
}, },
}, },
@ -83,6 +89,15 @@ export default {
}; };
}, },
}, },
watch: {
confirmLoading(isLoading, wasLoading) {
// If the button was loading and now no longer is
if (!isLoading && wasLoading) {
// Hide the modal
this.$emit('change', false);
}
},
},
}; };
</script> </script>
@ -94,7 +109,7 @@ export default {
title-class="gl-text-red-500" title-class="gl-text-red-500"
:action-primary="modalActionProps.primary" :action-primary="modalActionProps.primary"
:action-cancel="modalActionProps.cancel" :action-cancel="modalActionProps.cancel"
@primary="$emit('primary', $event)" @primary.prevent="$emit('primary')"
@change="$emit('change', $event)" @change="$emit('change', $event)"
> >
<template #modal-title>{{ $options.i18n.title }}</template> <template #modal-title>{{ $options.i18n.title }}</template>

View File

@ -20,6 +20,9 @@ export default {
computed: { computed: {
...mapState(['artifacts', 'isLoading', 'hasError']), ...mapState(['artifacts', 'isLoading', 'hasError']),
...mapGetters(['title']), ...mapGetters(['title']),
hasArtifacts() {
return this.artifacts.length > 0;
},
}, },
created() { created() {
this.setEndpoint(this.endpoint); this.setEndpoint(this.endpoint);
@ -31,7 +34,12 @@ export default {
}; };
</script> </script>
<template> <template>
<mr-collapsible-extension :title="title" :is-loading="isLoading" :has-error="hasError"> <mr-collapsible-extension
v-if="isLoading || hasArtifacts || hasError"
:title="title"
:is-loading="isLoading"
:has-error="hasError"
>
<artifacts-list :artifacts="artifacts" /> <artifacts-list :artifacts="artifacts" />
</mr-collapsible-extension> </mr-collapsible-extension>
</template> </template>

View File

@ -198,6 +198,9 @@ export default {
hasActionDelete() { hasActionDelete() {
return this.project.availableActions?.includes(ACTION_DELETE); return this.project.availableActions?.includes(ACTION_DELETE);
}, },
isActionDeleteLoading() {
return this.project.actionLoadingStates[ACTION_DELETE];
},
}, },
methods: { methods: {
topicPath(topic) { topicPath(topic) {
@ -390,6 +393,7 @@ export default {
v-model="isDeleteModalVisible" v-model="isDeleteModalVisible"
:confirm-phrase="project.name" :confirm-phrase="project.name"
:is-fork="project.isForked" :is-fork="project.isForked"
:confirm-loading="isActionDeleteLoading"
:merge-requests-count="openMergeRequestsCount" :merge-requests-count="openMergeRequestsCount"
:issues-count="openIssuesCount" :issues-count="openIssuesCount"
:forks-count="forksCount" :forks-count="forksCount"

View File

@ -12,7 +12,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22' ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :web_ide_clientside_preview_enabled, remove_with: '15.11', remove_after: '2023-04-22' ignore_column :web_ide_clientside_preview_enabled, remove_with: '15.11', remove_after: '2023-04-22'
ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22' ignore_columns %i[instance_administration_project_id instance_administrators_group_id], remove_with: '16.2', remove_after: '2023-06-22'
ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.10', remove_after: '2024-03-22'
ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21' ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21'
ignore_columns %i[delayed_project_removal lock_delayed_project_removal delayed_group_deletion], remove_with: '16.10', remove_after: '2024-03-22' ignore_columns %i[delayed_project_removal lock_delayed_project_removal delayed_group_deletion], remove_with: '16.10', remove_after: '2024-03-22'

View File

@ -21,8 +21,9 @@ module Ci
when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc
when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc
when 'created_at_asc' then relation.order_by_created_at_asc when 'created_at_asc' then relation.order_by_created_at_asc
when 'created_at_desc' then relation.order_by_created_at_desc
else else
relation.order_by_created_at_desc relation.order_by_star_count(:desc)
end end
end end

View File

@ -10,6 +10,7 @@ module Ci
class Resource < ::ApplicationRecord class Resource < ::ApplicationRecord
include PgFullTextSearchable include PgFullTextSearchable
include Gitlab::VisibilityLevel include Gitlab::VisibilityLevel
include Sortable
self.table_name = 'catalog_resources' self.table_name = 'catalog_resources'
@ -35,6 +36,15 @@ module Ci
scope :order_by_name_asc, -> { reorder(arel_table[:name].asc.nulls_last) } scope :order_by_name_asc, -> { reorder(arel_table[:name].asc.nulls_last) }
scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) } scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) } scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
scope :order_by_star_count, ->(direction) do
build_keyset_order_on_joined_column(
scope: joins(:project),
attribute_name: 'project_star_count',
column: Project.arel_table[:star_count],
direction: direction,
nullable: :nulls_last
)
end
delegate :avatar_path, :star_count, :full_path, to: :project delegate :avatar_path, :star_count, :full_path, to: :project

View File

@ -7,6 +7,10 @@ module Ci
# Only versions which contain valid CI components are included in this table. # Only versions which contain valid CI components are included in this table.
class Version < ::ApplicationRecord class Version < ::ApplicationRecord
include BulkInsertableAssociations include BulkInsertableAssociations
include SemanticVersionable
semver_method :version
validate_semver
self.table_name = 'catalog_resource_versions' self.table_name = 'catalog_resource_versions'
@ -33,8 +37,6 @@ module Ci
after_save :update_catalog_resource after_save :update_catalog_resource
class << self class << self
# In the future, we should support semantic versioning.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/427286
def latest def latest
order_by_released_at_desc.first order_by_released_at_desc.first
end end

View File

@ -9,6 +9,14 @@ module Ci
include Limitable include Limitable
include EachBatch include EachBatch
include BatchNullifyDependentAssociations include BatchNullifyDependentAssociations
include Gitlab::Utils::StrongMemoize
VALID_REF_REGEX = %r{\A(#{Gitlab::Git::TAG_REF_PREFIX}|#{Gitlab::Git::BRANCH_REF_PREFIX}).+}
# The only way that ref can be unexpanded after #expand_short_ref runs is if the ref
# is ambiguous because both a branch and a tag with the name exist, or it is
# ambiguous because neither exists.
INVALID_REF_MESSAGE = 'is ambiguous'
self.limit_name = 'ci_pipeline_schedules' self.limit_name = 'ci_pipeline_schedules'
self.limit_scope = :project self.limit_scope = :project
@ -21,7 +29,8 @@ module Ci
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? },
format: { with: VALID_REF_REGEX, allow_nil: true, message: INVALID_REF_MESSAGE, unless: :importing? }
validates :description, presence: true validates :description, presence: true
validates :variables, nested_attributes_duplicates: true validates :variables, nested_attributes_duplicates: true
@ -33,6 +42,8 @@ module Ci
scope :owned_by, ->(user) { where(owner: user) } scope :owned_by, ->(user) { where(owner: user) }
scope :for_project, ->(project_id) { where(project_id: project_id) } scope :for_project, ->(project_id) { where(project_id: project_id) }
before_validation :expand_short_ref
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
alias_attribute :real_next_run, :next_run_at alias_attribute :real_next_run, :next_run_at
@ -91,6 +102,21 @@ module Ci
super super
end end
private
def expand_short_ref
return if ref.blank? || VALID_REF_REGEX.match?(ref) || ambiguous_ref?
# In case the ref doesn't exist default to the initial value
self.ref = project.repository.expand_ref(ref) || ref
end
def ambiguous_ref?
strong_memoize_with(:ambiguous_ref, ref) do
project.repository.ambiguous_ref?(ref)
end
end
end end
end end

View File

@ -6,16 +6,20 @@ module Ci
include Ci::HasVariable include Ci::HasVariable
include Ci::RawVariable include Ci::RawVariable
ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_pipeline_variables_routing_table
belongs_to :pipeline, belongs_to :pipeline,
->(pipeline_variable) { in_partition(pipeline_variable) }, ->(pipeline_variable) { in_partition(pipeline_variable) },
partition_foreign_key: :partition_id, partition_foreign_key: :partition_id,
inverse_of: :variables inverse_of: :variables
self.primary_key = :id self.primary_key = :id
self.table_name = :p_ci_pipeline_variables
self.sequence_name = :ci_pipeline_variables_id_seq self.sequence_name = :ci_pipeline_variables_id_seq
partitionable scope: :pipeline, partitioned: true partitionable scope: :pipeline, through: {
table: :p_ci_pipeline_variables,
flag: ROUTING_FEATURE_FLAG
}
alias_attribute :secret_value, :value alias_attribute :secret_value, :value

View File

@ -16,12 +16,22 @@ module Integrations
end end
def self.help def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer' docs_link = ActionController::Base.helpers.link_to(_('Learn more.'),
s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'),
target: '_blank',
rel: 'noopener noreferrer')
help = format(s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe,
docs_link: docs_link.html_safe)
help << "<br><br><i>#{attribution_notice}</i>".html_safe
end end
def self.to_param def self.to_param
'bugzilla' 'bugzilla'
end end
def self.attribution_notice
_('The Bugzilla logo is a trademark of the Mozilla Foundation in the U.S. and other countries.')
end
end end
end end

View File

@ -7,6 +7,7 @@ module Ml
include SemanticVersionable include SemanticVersionable
semver_method :semver semver_method :semver
validate_semver
validates :project, :model, presence: true validates :project, :model, presence: true
@ -63,6 +64,11 @@ module Ml
end end
end end
def version=(value)
self.semver = value
super(value)
end
private private
def valid_model? def valid_model?

View File

@ -42,6 +42,7 @@ class PersonalAccessToken < ApplicationRecord
scope :owner_is_human, -> { includes(:user).references(:user).merge(User.human) } scope :owner_is_human, -> { includes(:user).references(:user).merge(User.human) }
scope :last_used_before, -> (date) { where("last_used_at <= ?", date) } scope :last_used_before, -> (date) { where("last_used_at <= ?", date) }
scope :last_used_after, -> (date) { where("last_used_at >= ?", date) } scope :last_used_after, -> (date) { where("last_used_at >= ?", date) }
scope :expiring_and_not_notified_without_impersonation, -> { where(["(revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= :date) and impersonation = false", { date: DAYS_TO_EXPIRE.days.from_now.to_date }]) }
validates :scopes, presence: true validates :scopes, presence: true
validates :expires_at, presence: true, on: :create, unless: :allow_expires_at_to_be_empty? validates :expires_at, presence: true, on: :create, unless: :allow_expires_at_to_be_empty?

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class SentNotification < ApplicationRecord class SentNotification < ApplicationRecord
include IgnorableColumns
ignore_column %i[id_convert_to_bigint], remove_with: '17.0', remove_after: '2024-04-19'
belongs_to :project belongs_to :project
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User" belongs_to :recipient, class_name: "User"

View File

@ -168,6 +168,8 @@ class User < MainClusterwide::ApplicationRecord
has_many :emails has_many :emails
has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :personal_access_tokens, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :expiring_soon_and_unnotified_personal_access_tokens, -> { expiring_and_not_notified_without_impersonation }, class_name: 'PersonalAccessToken'
has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
has_many :webauthn_registrations has_many :webauthn_registrations
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@ -615,6 +617,12 @@ class User < MainClusterwide::ApplicationRecord
.where('keys.user_id = users.id') .where('keys.user_id = users.id')
.expiring_soon_and_not_notified) .expiring_soon_and_not_notified)
end end
scope :with_personal_access_tokens_expiring_soon_and_ids, ->(ids) do
where(id: ids)
.includes(:expiring_soon_and_unnotified_personal_access_tokens)
end
scope :order_recent_sign_in, -> { reorder(arel_table[:current_sign_in_at].desc.nulls_last) } scope :order_recent_sign_in, -> { reorder(arel_table[:current_sign_in_at].desc.nulls_last) }
scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) } scope :order_oldest_sign_in, -> { reorder(arel_table[:current_sign_in_at].asc.nulls_last) }
scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) } scope :order_recent_last_activity, -> { reorder(arel_table[:last_activity_on].desc.nulls_last, arel_table[:id].asc) }

View File

@ -35,7 +35,8 @@ module Ci
@version = Ci::Catalog::Resources::Version.new( @version = Ci::Catalog::Resources::Version.new(
release: release, release: release,
catalog_resource: project.catalog_resource, catalog_resource: project.catalog_resource,
project: project project: project,
version: release.tag
) )
end end

View File

@ -18,7 +18,7 @@
%p %p
= s_('SlackIntegration|You must do this step only once.') = s_('SlackIntegration|You must do this step only once.')
%p %p
= render Pajamas::ButtonComponent.new(href: slack_app_manifest_share_admin_application_settings_path) do = render Pajamas::ButtonComponent.new(href: slack_app_manifest_share_admin_application_settings_path, target: '_blank', button_options: { rel: 'noopener noreferrer' }) do
= s_("SlackIntegration|Create Slack app") = s_("SlackIntegration|Create Slack app")
%hr %hr
%h5 %h5
@ -63,4 +63,3 @@
%p %p
= render Pajamas::ButtonComponent.new(href: slack_app_manifest_download_admin_application_settings_path, icon: 'download') do = render Pajamas::ButtonComponent.new(href: slack_app_manifest_download_admin_application_settings_path, icon: 'download') do
= s_("SlackIntegration|Download latest manifest file") = s_("SlackIntegration|Download latest manifest file")

View File

@ -18,21 +18,18 @@ module PersonalAccessTokens
BATCH_SIZE = 100 BATCH_SIZE = 100
def perform(*args) def perform(*args)
limit_date = PersonalAccessToken::DAYS_TO_EXPIRE.days.from_now.to_date
# rubocop: disable CodeReuse/ActiveRecord -- We need to specify batch size to avoid timing out of worker # rubocop: disable CodeReuse/ActiveRecord -- We need to specify batch size to avoid timing out of worker
loop do loop do
tokens = PersonalAccessToken.without_impersonation.expiring_and_not_notified(limit_date) tokens = PersonalAccessToken.expiring_and_not_notified_without_impersonation
.select(:user_id).limit(BATCH_SIZE).to_a .select(:user_id).limit(BATCH_SIZE).to_a
break if tokens.empty? break if tokens.empty?
users = User.where(id: tokens.pluck(:user_id).uniq) users = User.with_personal_access_tokens_expiring_soon_and_ids(tokens.pluck(:user_id).uniq)
users.each do |user| users.each do |user|
with_context(user: user) do with_context(user: user) do
expiring_user_tokens = user.personal_access_tokens expiring_user_tokens = user.expiring_soon_and_unnotified_personal_access_tokens
.without_impersonation.expiring_and_not_notified(limit_date)
next if expiring_user_tokens.empty? next if expiring_user_tokens.empty?

View File

@ -0,0 +1,9 @@
---
name: ci_partitioning_use_ci_pipeline_variables_routing_table
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/439069
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/143334
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17508
milestone: '16.10'
group: group::pipeline execution
type: gitlab_com_derisk
default_enabled: false

View File

@ -9,7 +9,6 @@ Gitlab::Database::Partitioning.register_models(
Ci::RunnerManagerBuild, Ci::RunnerManagerBuild,
Ci::JobAnnotation, Ci::JobAnnotation,
Ci::BuildMetadata, Ci::BuildMetadata,
Ci::PipelineVariable,
CommitStatus, CommitStatus,
BatchedGitRefUpdates::Deletion, BatchedGitRefUpdates::Deletion,
Users::ProjectVisit, Users::ProjectVisit,

View File

@ -0,0 +1,138 @@
- name: GitLab Duo Chat Beta now available in Premium
description: |
In 16.8, we made GitLab Duo Chat available for self-managed instances. In 16.9, we are making Chat available to Premium customers while it is still in Beta.
GitLab Duo Chat can:
- Explain or summarize issues, epics, and code.
- Answer specific questions about these artifacts like "Collect all the arguments raised in comments regarding the solution proposed in this issue."
- Generate code or content based on the information in these artifacts. For example, "Can you write documentation for this code?"
- Help you start a process. For example, "Create a .gitlab-ci.yml configuration file for testing and building a Ruby on Rails application in a GitLab CI/CD pipeline."
- Answer all your DevSecOps related question, whether you are a beginner or an expert. For example, "How can I set up Dynamic Application Security Testing for a REST API?"
- Answer follow-up questions so you can iteratively work through all the previous scenarios.
GitLab Duo Chat is available as a [Beta](https://docs.gitlab.com/ee/policy/experiment-beta-support.html#beta) feature. It is also integrated into our Web IDE and GitLab Workflow extension for VS Code as [Experimental](https://docs.gitlab.com/ee/policy/experiment-beta-support.html#experiment) features. In these IDEs, you can also use [predefined chat commands that help you do standard tasks more quickly](https://docs.gitlab.com/ee/user/gitlab_duo_chat.html#explain-code-in-the-ide) like writing tests.
stage: ai-powered
self-managed: true
gitlab-com: true
available_in: [Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/gitlab_duo_chat.html
image_url: https://about.gitlab.com/images/16_9/gitlab_duo_chat_beta_now_available_in_premium.png
published_at: 2024-02-15
release: 16.9
- name: Improvements to the CI/CD variables user interface
description: |
In GitLab 16.9, we have released a series of improvements to the CI/CD variables user experience. We have improved the variables creation flow through changes including:
- [Improved validation when variable values do not meet the requirements](https://gitlab.com/gitlab-org/gitlab/-/issues/365934).
- [Help text during variable creation](https://gitlab.com/gitlab-org/gitlab/-/issues/410220).
- [Allow resizing of the value field in the variables form](https://gitlab.com/gitlab-org/gitlab/-/issues/434667).
Other improvements include a new, [optional description field for group and project variables](https://gitlab.com/gitlab-org/gitlab/-/issues/378938) to assist with the management of variables. We have also made it easier to [add or edit multiple variables](https://gitlab.com/gitlab-org/gitlab/-/issues/434666), lowering the friction in the software development workflow and enabling developers to perform their job more efficiently.
Your [feedback for these changes](https://gitlab.com/gitlab-org/gitlab/-/issues/441177) is always valued and appreciated.
stage: verify
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/ci/variables/
image_url: https://img.youtube.com/vi/gdL2cEp3kw0/hqdefault.jpg
published_at: 2024-02-15
release: 16.9
- name: Request changes on merge requests
description: |
The last part of reviewing a merge request is communicating the outcome. While approving was unambiguous, leaving comments was not. They required the author to read your comments, then determine if the comments were purely informational, or described needed changes. Now, when you complete your review, you can select from three options:
- **Comment**: Submit general feedback without explicitly approving.
- **Approve**: Submit feedback and approve the changes.
- **Request changes**: Submit feedback that should be addressed before merging.
The sidebar now shows the outcome of your review next to your name. Currently, ending your review with **Request changes** doesn't block the merge request from being merged, but it provides extra context to other participants in the merge request.
You can leave feedback about the **Request changes** feature in our [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/438573).
stage: create
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#submit-a-review
image_url: https://about.gitlab.com/images/16_9/create-request-changes-merge-requests.png
published_at: 2024-02-15
release: 16.9
- name: Expanded options for auto-canceling pipelines
description: |
Currently, to use the [auto-cancel redundant pipeline feature](https://docs.gitlab.com/ee/ci/pipelines/settings.html#auto-cancel-redundant-pipelines), you must set jobs that can be cancelled as [`interruptible: true`](https://docs.gitlab.com/ee/ci/yaml/index.html#interruptible) to determine whether or not a pipeline can be cancelled. But this only applies to jobs that are actively running when GitLab tries to cancel the pipeline. Any jobs that have not yet started (are in "pending" status) are also considered safe to cancel, regardless of their `interruptible` configuration.
This lack of flexibility hinders users who want more control over which exact jobs can be cancelled by the auto-cancel pipeline feature. To address this limitation, we are pleased to announce the introduction of the `auto_cancel:on_new_commit` keywords with more granular control over job cancellation. If the legacy behavior did not work for you, you now have the option to configure the pipeline to only cancel jobs that are explicitly set with `interruptible: true`, even if they haven't started yet. You can also set jobs to never be automatically cancelled.
stage: verify
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/ci/yaml/index.html#workflowauto_cancelon_new_commit
image_url: https://about.gitlab.com/images/16_9/interruptible.png
published_at: 2024-02-15
release: 16.9
- name: Validate Terraform modules from your group or subgroup
description: |
When using the GitLab Terraform registry, it is important to have a cross-project view of all your modules. Until recently, the user interface has been available only at the project level. If your group had a complex structure, you might have had difficulty finding and validating your modules.
From GitLab 16.9, you can view all of your group and subgroup modules in GitLab. The increased visibility provides a better understanding of your registry, and decreases the likelihood of name collisions.
stage: package
self-managed: true
gitlab-com: true
available_in: [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/packages/package_registry/#view-packages
image_url: https://img.youtube.com/vi/1Ocypvrrdiw/hqdefault.jpg
published_at: 2024-02-15
release: 16.9
- name: More detailed security findings in VS Code
description: |
We've improved how security findings are shown in the [GitLab Workflow extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow#security-findings) for Visual Studio Code (VS Code).
You can now see more details of your security findings that weren't previously shown, including:
- Full descriptions, with rich-text formatting.
- The solution to the vulnerability, if one is available.
- A link to the location where the problem occurs in your codebase.
- Links to more information about the type of vulnerability discovered.
We've also:
- Improved how the extension shows the status of security scans before results are ready.
- Made other usability improvements.
stage: secure
self-managed: true
gitlab-com: true
available_in: [Ultimate]
documentation_link: https://docs.gitlab.com/ee/editor_extensions/visual_studio_code/
image_url: https://about.gitlab.com/images/16_9/vs-code-security-finding-details.png
published_at: 2024-02-15
release: 16.9
- name: Standards Adherence Report Improvements
description: |
The [standards adherence report](https://docs.gitlab.com/ee/user/compliance/compliance_center/#view-the-standards-adherence-dashboard), within the
[compliance center](https://docs.gitlab.com/ee/user/compliance/compliance_center/), is the destination for compliance teams to monitor their compliance posture.
In GitLab 16.5, we introduced the report with the GitLab Standard - a set of common compliance requirements all compliance teams should monitor. The standard helps
you understand which projects meet these requirements, which ones fall short, and how to bring them into compliance. Over time, we'll be introducing more standards
into the reporting.
In this milestone, we've made some improvements which will make reporting more robust and actionable. These include:
- Grouping results by project
- Grouping results by standard (starting with the GitLab standard)
- Filtering by project, compliance framework, name, and standard
- Export to CSV (delivered via email)
- Improved pagination
stage: govern
self-managed: true
gitlab-com: true
available_in: [Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/compliance/compliance_center/#standards-adherence-dashboard
image_url: https://about.gitlab.com/images/16_9/standards-adherence-grouping.png
published_at: 2024-02-15
release: 16.9

View File

@ -7,4 +7,19 @@ feature_categories:
description: Verification status for DAST Profiles description: Verification status for DAST Profiles
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103063 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103063
milestone: '15.6' milestone: '15.6'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_profile_id
table: dast_profiles
sharding_key: project_id
belongs_to: dast_profile

View File

@ -7,4 +7,19 @@ feature_categories:
description: Join table between DAST Profiles and CI Pipelines description: Join table between DAST Profiles and CI Pipelines
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56821 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56821
milestone: '13.11' milestone: '13.11'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_profile_id
table: dast_profiles
sharding_key: project_id
belongs_to: dast_profile

View File

@ -7,4 +7,19 @@ feature_categories:
description: Join Table for Runner tags and DAST Profiles description: Join Table for Runner tags and DAST Profiles
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108371 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108371
milestone: '15.8' milestone: '15.8'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_profile_id
table: dast_profiles
sharding_key: project_id
belongs_to: dast_profile

View File

@ -7,4 +7,19 @@ feature_categories:
description: Join table between DAST Scanner Profiles and CI Builds description: Join table between DAST Scanner Profiles and CI Builds
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63362 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63362
milestone: '14.1' milestone: '14.1'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_scanner_profile_id
table: dast_scanner_profiles
sharding_key: project_id
belongs_to: dast_scanner_profile

View File

@ -7,4 +7,19 @@ feature_categories:
description: Secret variables used in DAST on-demand scans description: Secret variables used in DAST on-demand scans
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56067 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56067
milestone: '13.11' milestone: '13.11'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_site_profile_id
table: dast_site_profiles
sharding_key: project_id
belongs_to: dast_site_profile

View File

@ -7,4 +7,19 @@ feature_categories:
description: Join table between DAST Site Profiles and CI Builds description: Join table between DAST Site Profiles and CI Builds
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63362 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63362
milestone: '14.1' milestone: '14.1'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_site_profile_id
table: dast_site_profiles
sharding_key: project_id
belongs_to: dast_site_profile

View File

@ -7,4 +7,19 @@ feature_categories:
description: The site to be validated with a dast_site_token description: The site to be validated with a dast_site_token
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41639 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41639
milestone: '13.4' milestone: '13.4'
gitlab_schema: gitlab_main gitlab_schema: gitlab_main_cell
allow_cross_joins:
- gitlab_main_clusterwide
allow_cross_transactions:
- gitlab_main_clusterwide
allow_cross_foreign_keys:
- gitlab_main_clusterwide
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: dast_site_token_id
table: dast_site_tokens
sharding_key: project_id
belongs_to: dast_site_token

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddSemVerToCatalogResourcesVersion < Gitlab::Database::Migration[2.2]
enable_lock_retries!
milestone '16.10'
# rubocop:disable Migration/AddLimitToTextColumns -- limit is added in 20240213113719_add_text_limit_to_catalog_resource_versions_semver_prerelease
def change
add_column :catalog_resource_versions, :semver_major, :integer
add_column :catalog_resource_versions, :semver_minor, :integer
add_column :catalog_resource_versions, :semver_patch, :integer
add_column :catalog_resource_versions, :semver_prerelease, :text
end
# rubocop:enable Migration/AddLimitToTextColumns
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddTextLimitToCatalogResourceVersionsSemverPrerelease < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.10'
def up
add_text_limit :catalog_resource_versions, :semver_prerelease, 255
end
def down
remove_text_limit :catalog_resource_versions, :semver_prerelease
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class SelfHostedSentNotificationsCleanup < Gitlab::Database::Migration[2.2]
include Gitlab::Database::MigrationHelpers::ConvertToBigint
enable_lock_retries!
milestone '16.10'
TABLE = :sent_notifications
COLUMNS = [:id]
def up
return if should_skip?
return if temp_column_removed?(TABLE, COLUMNS.first)
cleanup_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
def down
# no-op
end
def should_skip?
com_or_dev_or_test_but_not_jh?
end
end

View File

@ -0,0 +1 @@
70164c8c55ac94314a73074b04ec1fc1ad4aaed199347f22904e6691aee870d3

View File

@ -0,0 +1 @@
eea390222d35a37a46a1f8997a2b9752f393b836cdb1db9ef45114f5260907b3

View File

@ -0,0 +1 @@
07e1a3a02552425f4a5345d9ed3eb7da7f4f09b9f6c9071b1527a1a0e5e3fd10

View File

@ -5516,7 +5516,12 @@ CREATE TABLE catalog_resource_versions (
catalog_resource_id bigint NOT NULL, catalog_resource_id bigint NOT NULL,
project_id bigint NOT NULL, project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
released_at timestamp with time zone DEFAULT '1970-01-01 00:00:00+00'::timestamp with time zone NOT NULL released_at timestamp with time zone DEFAULT '1970-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
semver_major integer,
semver_minor integer,
semver_patch integer,
semver_prerelease text,
CONSTRAINT check_701bdce47b CHECK ((char_length(semver_prerelease) <= 255))
); );
CREATE SEQUENCE catalog_resource_versions_id_seq CREATE SEQUENCE catalog_resource_versions_id_seq

View File

@ -249,6 +249,7 @@ Example response:
"is_shared": false, "is_shared": false,
"runner_type": "project_type", "runner_type": "project_type",
"contacted_at": "2016-01-25T16:39:48.066Z", "contacted_at": "2016-01-25T16:39:48.066Z",
"maintenance_note": null,
"name": null, "name": null,
"online": true, "online": true,
"status": "online", "status": "online",
@ -281,17 +282,18 @@ Update details of a runner.
PUT /runners/:id PUT /runners/:id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
|-------------------|---------|----------|-------------------------------------------------------------------------------------------------| |--------------------|---------|----------|-------------------------------------------------------------------------------------------------|
| `id` | integer | yes | The ID of a runner | | `id` | integer | yes | The ID of a runner |
| `description` | string | no | The description of the runner | | `description` | string | no | The description of the runner |
| `active` | boolean | no | Deprecated: Use `paused` instead. Flag indicating whether the runner is allowed to receive jobs | | `active` | boolean | no | Deprecated: Use `paused` instead. Flag indicating whether the runner is allowed to receive jobs |
| `paused` | boolean | no | Specifies if the runner should ignore new jobs | | `paused` | boolean | no | Specifies if the runner should ignore new jobs |
| `tag_list` | array | no | The list of tags for the runner | | `tag_list` | array | no | The list of tags for the runner |
| `run_untagged` | boolean | no | Specifies if the runner can execute untagged jobs | | `run_untagged` | boolean | no | Specifies if the runner can execute untagged jobs |
| `locked` | boolean | no | Specifies if the runner is locked | | `locked` | boolean | no | Specifies if the runner is locked |
| `access_level` | string | no | The access level of the runner; `not_protected` or `ref_protected` | | `access_level` | string | no | The access level of the runner; `not_protected` or `ref_protected` |
| `maximum_timeout` | integer | no | Maximum timeout that limits the amount of time (in seconds) that runners can run jobs | | `maximum_timeout` | integer | no | Maximum timeout that limits the amount of time (in seconds) that runners can run jobs |
| `maintenance_note` | string | no | Free-form maintenance notes for the runner (1024 characters) |
```shell ```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" \ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" \
@ -318,6 +320,7 @@ Example response:
"is_shared": false, "is_shared": false,
"runner_type": "group_type", "runner_type": "group_type",
"contacted_at": "2016-01-25T16:39:48.066Z", "contacted_at": "2016-01-25T16:39:48.066Z",
"maintenance_note": null,
"name": null, "name": null,
"online": true, "online": true,
"status": "online", "status": "online",

View File

@ -260,7 +260,7 @@ To publish a new version of the component to the catalog:
running the release job. running the release job.
After the release job completes successfully, the release is created and the new version After the release job completes successfully, the release is created and the new version
is published to the CI/CD catalog. is published to the CI/CD catalog. Tags must use semantic versioning, for example `1.0.0`.
### Unpublish a component project ### Unpublish a component project

View File

@ -85,9 +85,9 @@ gdk start
tail -f log/llm.log tail -f log/llm.log
``` ```
## Testing GitLab Duo Chat against real LLMs locally ## Testing GitLab Duo Chat
Because success of answers to user questions in GitLab Duo Chat heavily depends Because the success of answers to user questions in GitLab Duo Chat heavily depends
on toolchain and prompts of each tool, it's common that even a minor change in a on toolchain and prompts of each tool, it's common that even a minor change in a
prompt or a tool impacts processing of some questions. prompt or a tool impacts processing of some questions.
@ -95,6 +95,25 @@ To make sure that a change in the toolchain doesn't break existing
functionality, you can use the following RSpec tests to validate answers to some functionality, you can use the following RSpec tests to validate answers to some
predefined questions when using real LLMs: predefined questions when using real LLMs:
1. `ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb`
This test validates that the zero-shot agent is selecting the correct tools
for a set of Chat questions. It checks on the tool selection but does not
evaluate the quality of the Chat response.
1. `ee/spec/lib/gitlab/llm/chain/agents/zero_shot/qa_evaluation_spec.rb`
This test evaluates the quality of a Chat response by passing the question
asked along with the Chat-provided answer and context to at least two other
LLMs for evaluation. This evaluation is limited to questions about issues and
epics only. Learn more about the [GitLab Duo Chat QA Evaluation Test](#gitlab-duo-chat-qa-evaluation-test).
If you are working on any changes to the GitLab Duo Chat logic, be sure to run
the [GitLab Duo Chat CI jobs](#testing-with-ci) the merge request that contains
your changes. Some of the CI jobs must be [manually triggered](../../ci/jobs/job_control.md#run-a-manual-job).
## Testing locally
To run the QA Evaluation test locally, the following environment variables
must be exported:
```ruby ```ruby
export VERTEX_AI_EMBEDDINGS='true' # if using Vertex embeddings export VERTEX_AI_EMBEDDINGS='true' # if using Vertex embeddings
export ANTHROPIC_API_KEY='<key>' # can use dev value of Gitlab::CurrentSettings export ANTHROPIC_API_KEY='<key>' # can use dev value of Gitlab::CurrentSettings
@ -104,21 +123,22 @@ export VERTEX_AI_PROJECT='<vertex-project-name>' # can use dev value of Gitlab::
REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb REAL_AI_REQUEST=1 bundle exec rspec ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb
``` ```
When you need to update the test questions that require documentation embeddings, When you update the test questions that require documentation embeddings,
make sure a new fixture is generated and committed together with the change. make sure you [generate a new fixture](index.md#use-embeddings-in-specs) and
commit it together with the change.
## Running the rspecs tagged with `real_ai_request` ## Testing with CI
The following CI jobs for GitLab project run the rspecs tagged with `real_ai_request`: The following CI jobs for GitLab project run the tests tagged with `real_ai_request`:
- `rspec-ee unit gitlab-duo-chat-zeroshot`: - `rspec-ee unit gitlab-duo-chat-zeroshot`:
the job runs `ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb`. the job runs `ee/spec/lib/gitlab/llm/completions/chat_real_requests_spec.rb`.
The job is optionally triggered and allowed to fail. The job must be manually triggered and is allowed to fail.
- `rspec-ee unit gitlab-duo-chat-qa`: - `rspec-ee unit gitlab-duo-chat-qa`:
The job runs the QA evaluation tests in The job runs the QA evaluation tests in
`ee/spec/lib/gitlab/llm/chain/agents/zero_shot/qa_evaluation_spec.rb`. `ee/spec/lib/gitlab/llm/chain/agents/zero_shot/qa_evaluation_spec.rb`.
The job is optionally triggered and allowed to fail. The job must be manually triggered and is allowed to fail.
Read about [GitLab Duo Chat QA Evaluation Test](#gitlab-duo-chat-qa-evaluation-test). Read about [GitLab Duo Chat QA Evaluation Test](#gitlab-duo-chat-qa-evaluation-test).
- `rspec-ee unit gitlab-duo-chat-qa-fast`: - `rspec-ee unit gitlab-duo-chat-qa-fast`:
@ -179,25 +199,30 @@ See [the snippet](https://gitlab.com/gitlab-org/gitlab/-/snippets/3613745) used
1. For each question, RSpec will regex-match for `CORRECT` or `INCORRECT`. 1. For each question, RSpec will regex-match for `CORRECT` or `INCORRECT`.
#### Collection and tracking of QA evaluations via CI/CD automation #### Collection and tracking of QA evaluation with CI/CD automation
The `gitlab` project's CI configurations have been setup to The `gitlab` project's CI configurations have been setup to run the RSpec,
run the RSpec, collect the evaluation response as artifacts and execute
collect the evaluation response as artifacts [a reporter script](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/duo_chat/reporter.rb)
and execute [a reporter script](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/duo_chat/reporter.rb)
that automates collection and tracking of evaluations. that automates collection and tracking of evaluations.
When `rspec-ee unit gitlab-duo-chat-qa` job runs in a pipeline for a merge request, When `rspec-ee unit gitlab-duo-chat-qa` job runs in a pipeline for a merge request,
the reporter script uses the evaluations saved as CI artifacts the reporter script uses the evaluations saved as CI artifacts
to generate a Markdown report and posts it as a note in the merge request. to generate a Markdown report and posts it as a note in the merge request.
When `rspec-ee unit gitlab-duo-chat-qa` is run in a pipeline for a commit on `master` branch, To keep track of and compare QA test results over time, you must manually
the reporter script instead run the `rspec-ee unit gitlab-duo-chat-qa` on the `master` the branch:
posts the generated report as an issue,
saves the evaluations artfacts as a snippet, 1. Visit the [new pipeline page](https://gitlab.com/gitlab-org/gitlab/-/pipelines/new).
and updates the tracking issue in 1. Select "Run pipeline" to run a pipeline against the `master` branch
[`gitlab-org/ai-powered/ai-framework/qa-evaluation#1`](https://gitlab.com/gitlab-org/ai-powered/ai-framework/qa-evaluation/-/issues/1) 1. When the pipeline first starts, the `rspec-ee unit gitlab-duo-chat-qa` job under the
in the project [`gitlab-org/ai-powered/ai-framework/qa-evaluation`](https://gitlab.com/gitlab-org/ai-powered/ai-framework/qa-evaluation). "Test" stage will not be available. Wait a few minutes for other CI jobs to
run and then manually kick off this job by selecting the "Play" icon.
When the test runs on `master`, the reporter script posts the generated report as an issue,
saves the evaluations artfacts as a snippet, and updates the tracking issue in
[`GitLab-org/ai-powered/ai-framework/qa-evaluation#1`](https://gitlab.com/gitlab-org/ai-powered/ai-framework/qa-evaluation/-/issues/1)
in the project [`GitLab-org/ai-powered/ai-framework/qa-evaluation`](<https://gitlab.com/gitlab-org/ai-powered/ai-framework/qa-evaluation>).
## GraphQL Subscription ## GraphQL Subscription

View File

@ -112,9 +112,14 @@ Gitlab::CurrentSettings.update!(anthropic_api_key: <insert API key>)
### Embeddings database ### Embeddings database
Embeddings are generated through the [VertexAI text embeddings API](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings). The sections Embeddings are generated through the [VertexAI text embeddings API](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings).
below explain how to populate embeddings in the DB or extract embeddings to be
used in specs. Embeddings for GitLab documentation are updated based on the latest changes
Monday through Friday at 05:00 UTC when the
[embeddings cron job](https://gitlab.com/gitlab-org/gitlab/-/blob/6742f6bd3970c56a9d5bcd31e3d3dff180c97088/config/initializers/1_settings.rb#L817) runs.
The sections below explain how to populate embeddings in the DB or extract
embeddings to be used in specs.
#### Set up #### Set up

View File

@ -14,7 +14,8 @@ In order to use SemanticVersionable you must first create a database migration t
```ruby ```ruby
class AddVersionPartsToModelVersions < Gitlab::Database::Migration[2.2] class AddVersionPartsToModelVersions < Gitlab::Database::Migration[2.2]
disable_ddl_transaction! enable_lock_retries!
milestone '16.9' milestone '16.9'
def up def up

View File

@ -180,7 +180,9 @@ module API
optional :maximum_timeout, type: Integer, optional :maximum_timeout, type: Integer,
desc: 'Maximum timeout that limits the amount of time (in seconds) ' \ desc: 'Maximum timeout that limits the amount of time (in seconds) ' \
'that runners can run jobs' 'that runners can run jobs'
at_least_one_of :description, :active, :paused, :tag_list, :run_untagged, :locked, :access_level, :maximum_timeout optional :maintenance_note, type: String,
desc: %q(Free-form maintenance notes for the runner (1024 characters))
at_least_one_of :description, :active, :paused, :tag_list, :run_untagged, :locked, :access_level, :maximum_timeout, :maintenance_note
mutually_exclusive :active, :paused mutually_exclusive :active, :paused
end end
put ':id' do put ':id' do

View File

@ -11,6 +11,7 @@ module API
expose :access_level expose :access_level
expose :version, :revision, :platform, :architecture expose :version, :revision, :platform, :architecture
expose :contacted_at expose :contacted_at
expose :maintenance_note
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options| expose :projects, with: Entities::BasicProjectDetails do |runner, options|

View File

@ -106,9 +106,7 @@ module Search
end end
def show_code_search_tab? def show_code_search_tab?
return true if tab_enabled_for_project?(:blobs) tab_enabled_for_project?(:blobs)
project.nil? && show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_code_tab)
end end
def show_wiki_search_tab? def show_wiki_search_tab?

View File

@ -28566,6 +28566,9 @@ msgstr ""
msgid "KubernetesDashboard|Services" msgid "KubernetesDashboard|Services"
msgstr "" msgstr ""
msgid "KubernetesDashboard|Spec"
msgstr ""
msgid "KubernetesDashboard|StatefulSet" msgid "KubernetesDashboard|StatefulSet"
msgstr "" msgstr ""
@ -33798,12 +33801,18 @@ msgstr ""
msgid "ObservabilityMetrics|Search metrics starting with..." msgid "ObservabilityMetrics|Search metrics starting with..."
msgstr "" msgstr ""
msgid "ObservabilityMetrics|Select attributes"
msgstr ""
msgid "ObservabilityMetrics|Type" msgid "ObservabilityMetrics|Type"
msgstr "" msgstr ""
msgid "ObservabilityMetrics|Value" msgid "ObservabilityMetrics|Value"
msgstr "" msgstr ""
msgid "ObservabilityMetrics|all"
msgstr ""
msgid "ObservabilityMetrics|is like" msgid "ObservabilityMetrics|is like"
msgstr "" msgstr ""
@ -34519,6 +34528,9 @@ msgstr ""
msgid "Organization|An error occurred creating an organization. Please try again." msgid "Organization|An error occurred creating an organization. Please try again."
msgstr "" msgstr ""
msgid "Organization|An error occurred deleting the project. Please refresh the page to try again."
msgstr ""
msgid "Organization|An error occurred loading the groups. Please refresh the page to try again." msgid "Organization|An error occurred loading the groups. Please refresh the page to try again."
msgstr "" msgstr ""
@ -49632,6 +49644,9 @@ msgstr[1] ""
msgid "The API key used by GitLab for accessing the Spam Check service endpoint." msgid "The API key used by GitLab for accessing the Spam Check service endpoint."
msgstr "" msgstr ""
msgid "The Bugzilla logo is a trademark of the Mozilla Foundation in the U.S. and other countries."
msgstr ""
msgid "The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment." msgid "The CSV export will be created in the background. Once finished, it will be sent to %{email} in an attachment."
msgstr "" msgstr ""

View File

@ -61,7 +61,7 @@
"@gitlab/favicon-overlay": "2.0.0", "@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0", "@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.83.0", "@gitlab/svgs": "3.83.0",
"@gitlab/ui": "^74.6.0", "@gitlab/ui": "^74.7.0",
"@gitlab/visual-review-tools": "1.7.3", "@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "^0.0.1-dev-20240214084918", "@gitlab/web-ide": "^0.0.1-dev-20240214084918",
"@mattiasbuelens/web-streams-adapter": "^0.1.0", "@mattiasbuelens/web-streams-adapter": "^0.1.0",

View File

@ -255,7 +255,7 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
context 'when a pipeline schedule has no variables' do context 'when a pipeline schedule has no variables' do
let(:basic_param) do let(:basic_param) do
{ description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'patch-x', active: true } { description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'master', active: true }
end end
context 'when params include one variable' do context 'when params include one variable' do
@ -309,7 +309,7 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
context 'when a pipeline schedule has one variable' do context 'when a pipeline schedule has one variable' do
let(:basic_param) do let(:basic_param) do
{ description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'patch-x', active: true } { description: 'updated_desc', cron: '0 1 * * *', cron_timezone: 'UTC', ref: 'master', active: true }
end end
let!(:pipeline_schedule_variable) do let!(:pipeline_schedule_variable) do

View File

@ -2,6 +2,8 @@
FactoryBot.define do FactoryBot.define do
factory :ci_catalog_resource_version, class: 'Ci::Catalog::Resources::Version' do factory :ci_catalog_resource_version, class: 'Ci::Catalog::Resources::Version' do
version { '1.0.0' }
catalog_resource factory: :ci_catalog_resource catalog_resource factory: :ci_catalog_resource
project { catalog_resource.project } project { catalog_resource.project }
release { association :release, project: project } release { association :release, project: project }

View File

@ -4,7 +4,7 @@ FactoryBot.define do
factory :ci_pipeline_schedule, class: 'Ci::PipelineSchedule' do factory :ci_pipeline_schedule, class: 'Ci::PipelineSchedule' do
cron { '0 1 * * *' } cron { '0 1 * * *' }
cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE } cron_timezone { Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE }
ref { 'master' } ref { "#{Gitlab::Git::BRANCH_REF_PREFIX}master" }
active { true } active { true }
description { "pipeline schedule" } description { "pipeline schedule" }
project project

View File

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'CI/CD Catalog releases', :js, feature_category: :pipeline_composition do RSpec.describe 'CI/CD Catalog releases', :js, feature_category: :pipeline_composition, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/432824' do
let_it_be(:tag_name) { 'catalog_release_tag' } let_it_be(:tag_name) { 'catalog_release_tag' }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be_with_reload(:namespace) { create(:group) } let_it_be_with_reload(:namespace) { create(:group) }

View File

@ -39,11 +39,11 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli
end end
context 'with name parameter' do context 'with name parameter' do
let(:name) { 'v1.0' } let(:name) { '1.0.0' }
it 'returns the version that matches the name' do it 'returns the version that matches the name' do
expect(execute.count).to eq(1) expect(execute.count).to eq(1)
expect(execute.first.name).to eq('v1.0') expect(execute.first.name).to eq('1.0.0')
end end
context 'when no version matches the name' do context 'when no version matches the name' do

View File

@ -81,6 +81,22 @@ describe('~/api/projects_api.js', () => {
}); });
}); });
describe('deleteProject', () => {
beforeEach(() => {
jest.spyOn(axios, 'delete');
});
it('deletes to the correct URL', () => {
const expectedUrl = `/api/v7/projects/${projectId}`;
mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
return projectsApi.deleteProject(projectId).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
});
});
});
describe('importProjectMembers', () => { describe('importProjectMembers', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(axios, 'post'); jest.spyOn(axios, 'post');

View File

@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlCollapse, GlButton } from '@gitlab/ui';
import WorkloadDetailsItem from '~/kubernetes_dashboard/components/workload_details_item.vue'; import WorkloadDetailsItem from '~/kubernetes_dashboard/components/workload_details_item.vue';
let wrapper; let wrapper;
@ -6,29 +7,87 @@ let wrapper;
const propsData = { const propsData = {
label: 'name', label: 'name',
}; };
const slots = { const defaultSlots = {
default: '<b>slot value</b>', default: '<b>slot value</b>',
label: `<label>${propsData.label}</label>`,
}; };
const createWrapper = () => { const createWrapper = ({ collapsible, slots = defaultSlots } = {}) => {
wrapper = shallowMount(WorkloadDetailsItem, { wrapper = shallowMount(WorkloadDetailsItem, {
propsData, propsData: {
...propsData,
collapsible,
},
slots, slots,
}); });
}; };
const findLabel = () => wrapper.findComponent('label'); const findLabel = () => wrapper.findComponent('label');
const findCollapsible = () => wrapper.findComponent(GlCollapse);
const findCollapsibleButton = () => wrapper.findComponent(GlButton);
describe('Workload details item component', () => { describe('Workload details item component', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper();
}); });
it('renders the correct label', () => { describe('by default', () => {
expect(findLabel().text()).toBe(propsData.label); beforeEach(() => {
createWrapper();
});
it('renders the correct label', () => {
expect(findLabel().text()).toBe(propsData.label);
});
it('renders default slot content', () => {
expect(wrapper.html()).toContain(defaultSlots.default);
});
}); });
it('renders slot content', () => { describe('when collapsible is true', () => {
expect(wrapper.html()).toContain(slots.default); beforeEach(() => {
createWrapper({ collapsible: true });
});
it('renders collapsible component that is not visible', () => {
expect(findCollapsible().props('visible')).toBe(false);
});
it('renders the collapsible button component', () => {
expect(findCollapsibleButton().props('icon')).toBe('chevron-right');
expect(findCollapsibleButton().attributes('aria-label')).toBe('Expand');
});
describe('when expanded', () => {
beforeEach(() => {
findCollapsibleButton().vm.$emit('click');
});
it('collapsible is visible', () => {
expect(findCollapsible().props('visible')).toBe(true);
});
it('updates the collapsible button component', () => {
expect(findCollapsibleButton().props('icon')).toBe('chevron-down');
expect(findCollapsibleButton().attributes('aria-label')).toBe('Collapse');
});
it('renders default slot content inside the collapsible', () => {
expect(findCollapsible().html()).toContain(defaultSlots.default);
});
});
});
describe('when label slot is provided', () => {
const labelSlot = '<span>custom value</span>';
beforeEach(() => {
createWrapper({ slots: { label: labelSlot } });
});
it('renders label slot content', () => {
expect(wrapper.html()).toContain(labelSlot);
});
}); });
}); });

View File

@ -7,7 +7,7 @@ import { mockPodsTableItems } from '../graphql/mock_data';
let wrapper; let wrapper;
const defaultItem = mockPodsTableItems[0]; const defaultItem = mockPodsTableItems[2];
const createWrapper = (item = defaultItem) => { const createWrapper = (item = defaultItem) => {
wrapper = shallowMount(WorkloadDetails, { wrapper = shallowMount(WorkloadDetails, {
@ -24,30 +24,51 @@ const findAllBadges = () => wrapper.findAllComponents(GlBadge);
const findBadge = (at) => findAllBadges().at(at); const findBadge = (at) => findAllBadges().at(at);
describe('Workload details component', () => { describe('Workload details component', () => {
beforeEach(() => { describe('when minimal fields are provided', () => {
createWrapper(); beforeEach(() => {
createWrapper();
});
it.each`
label | data | collapsible | index
${'Name'} | ${defaultItem.name} | ${false} | ${0}
${'Kind'} | ${defaultItem.kind} | ${false} | ${1}
${'Labels'} | ${'key=value'} | ${false} | ${2}
${'Status'} | ${defaultItem.status} | ${false} | ${3}
${'Annotations'} | ${'annotation: text\nanother: text'} | ${true} | ${4}
`('renders a list item for $label', ({ label, data, collapsible, index }) => {
expect(findWorkloadDetailsItem(index).props('label')).toBe(label);
expect(findWorkloadDetailsItem(index).text()).toMatchInterpolatedText(data);
expect(findWorkloadDetailsItem(index).props('collapsible')).toBe(collapsible);
});
it('renders a badge for each of the labels', () => {
const label = 'key=value';
expect(findAllBadges()).toHaveLength(2);
expect(findBadge(0).text()).toBe(label);
});
it('renders a badge for the status value', () => {
const { status } = defaultItem;
expect(findBadge(1).text()).toBe(status);
expect(findBadge(1).props('variant')).toBe(WORKLOAD_STATUS_BADGE_VARIANTS[status]);
});
}); });
it.each` describe('when additional fields are provided', () => {
label | data | index beforeEach(() => {
${'Name'} | ${defaultItem.name} | ${0} createWrapper(mockPodsTableItems[0]);
${'Kind'} | ${defaultItem.kind} | ${1} });
${'Labels'} | ${'key=value'} | ${2}
${'Status'} | ${defaultItem.status} | ${3}
${'Annotations'} | ${'annotation: text another: text'} | ${4}
`('renders a list item for each not empty value', ({ label, data, index }) => {
expect(findWorkloadDetailsItem(index).props('label')).toBe(label);
expect(findWorkloadDetailsItem(index).text()).toMatchInterpolatedText(data);
});
it('renders a badge for each of the labels', () => { it.each`
const label = 'key=value'; label | yaml | index
expect(findBadge(0).text()).toBe(label); ${'Status'} | ${'phase: Running\nready: true\nrestartCount: 4'} | ${3}
}); ${'Annotations'} | ${'annotation: text\nanother: text'} | ${4}
${'Spec'} | ${'restartPolicy: Never\nterminationGracePeriodSeconds: 30'} | ${5}
it('renders a badge for the status value', () => { `('renders a collapsible list item for $label with the yaml code', ({ label, yaml, index }) => {
const { status } = defaultItem; expect(findWorkloadDetailsItem(index).props('label')).toBe(label);
expect(findBadge(1).text()).toBe(status); expect(findWorkloadDetailsItem(index).text()).toBe(yaml);
expect(findBadge(1).props('variant')).toBe(WORKLOAD_STATUS_BADGE_VARIANTS[status]); expect(findWorkloadDetailsItem(index).props('collapsible')).toBe(true);
});
}); });
}); });

View File

@ -1,5 +1,5 @@
const runningPod = { const runningPod = {
status: { phase: 'Running' }, status: { phase: 'Running', ready: true, restartCount: 4 },
metadata: { metadata: {
name: 'pod-1', name: 'pod-1',
namespace: 'default', namespace: 'default',
@ -7,6 +7,7 @@ const runningPod = {
labels: { key: 'value' }, labels: { key: 'value' },
annotations: { annotation: 'text', another: 'text' }, annotations: { annotation: 'text', another: 'text' },
}, },
spec: { restartPolicy: 'Never', terminationGracePeriodSeconds: 30 },
}; };
const pendingPod = { const pendingPod = {
status: { phase: 'Pending' }, status: { phase: 'Pending' },
@ -14,9 +15,10 @@ const pendingPod = {
name: 'pod-2', name: 'pod-2',
namespace: 'new-namespace', namespace: 'new-namespace',
creationTimestamp: '2023-11-21T11:50:59Z', creationTimestamp: '2023-11-21T11:50:59Z',
labels: {}, labels: { key: 'value' },
annotations: {}, annotations: { annotation: 'text', another: 'text' },
}, },
spec: {},
}; };
const succeededPod = { const succeededPod = {
status: { phase: 'Succeeded' }, status: { phase: 'Succeeded' },
@ -27,6 +29,7 @@ const succeededPod = {
labels: {}, labels: {},
annotations: {}, annotations: {},
}, },
spec: {},
}; };
const failedPod = { const failedPod = {
status: { phase: 'Failed' }, status: { phase: 'Failed' },
@ -37,6 +40,7 @@ const failedPod = {
labels: {}, labels: {},
annotations: {}, annotations: {},
}, },
spec: {},
}; };
export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod]; export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod];
@ -69,24 +73,29 @@ export const mockPodsTableItems = [
labels: { key: 'value' }, labels: { key: 'value' },
annotations: { annotation: 'text', another: 'text' }, annotations: { annotation: 'text', another: 'text' },
kind: 'Pod', kind: 'Pod',
spec: { restartPolicy: 'Never', terminationGracePeriodSeconds: 30 },
fullStatus: { phase: 'Running', ready: true, restartCount: 4 },
}, },
{ {
name: 'pod-1', name: 'pod-1',
namespace: 'default', namespace: 'default',
status: 'Running', status: 'Running',
age: '114d', age: '114d',
labels: {}, labels: { key: 'value' },
annotations: {}, annotations: { annotation: 'text', another: 'text' },
kind: 'Pod', kind: 'Pod',
spec: {},
fullStatus: { phase: 'Running', ready: true, restartCount: 4 },
}, },
{ {
name: 'pod-2', name: 'pod-2',
namespace: 'new-namespace', namespace: 'new-namespace',
status: 'Pending', status: 'Pending',
age: '1d', age: '1d',
labels: {}, labels: { key: 'value' },
annotations: {}, annotations: { annotation: 'text', another: 'text' },
kind: 'Pod', kind: 'Pod',
spec: {},
}, },
{ {
name: 'pod-3', name: 'pod-3',
@ -96,6 +105,7 @@ export const mockPodsTableItems = [
labels: {}, labels: {},
annotations: {}, annotations: {},
kind: 'Pod', kind: 'Pod',
spec: {},
}, },
{ {
name: 'pod-4', name: 'pod-4',
@ -105,6 +115,7 @@ export const mockPodsTableItems = [
labels: {}, labels: {},
annotations: {}, annotations: {},
kind: 'Pod', kind: 'Pod',
spec: {},
}, },
{ {
name: 'pod-4', name: 'pod-4',
@ -114,6 +125,7 @@ export const mockPodsTableItems = [
labels: {}, labels: {},
annotations: {}, annotations: {},
kind: 'Pod', kind: 'Pod',
spec: {},
}, },
]; ];

View File

@ -6,7 +6,9 @@ import NewProjectButton from '~/organizations/shared/components/new_project_butt
import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql'; import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
import { formatProjects } from '~/organizations/shared/utils'; import { formatProjects } from '~/organizations/shared/utils';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
import { ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { createAlert } from '~/alert'; import { createAlert } from '~/alert';
import { deleteProject } from '~/api/projects_api';
import { DEFAULT_PER_PAGE } from '~/api'; import { DEFAULT_PER_PAGE } from '~/api';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
@ -19,6 +21,7 @@ import {
} from '~/organizations/mock_data'; } from '~/organizations/mock_data';
jest.mock('~/alert'); jest.mock('~/alert');
jest.mock('~/api/projects_api');
Vue.use(VueApollo); Vue.use(VueApollo);
@ -301,4 +304,80 @@ describe('ProjectsView', () => {
}); });
}); });
}); });
describe('Deleting project', () => {
const MOCK_PROJECT = formatProjects(nodes)[0];
describe('when API call is successful', () => {
beforeEach(async () => {
deleteProject.mockResolvedValueOnce(Promise.resolve());
createComponent();
jest.spyOn(wrapper.vm.$apollo.queries.projects, 'refetch');
await waitForPromises();
});
it('calls deleteProject and properly sets project.isDeleting to true before the promise resolves', () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
expect(deleteProject).toHaveBeenCalledWith(MOCK_PROJECT.id);
expect(MOCK_PROJECT.actionLoadingStates[ACTION_DELETE]).toBe(true);
});
it('does not call createAlert', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
expect(createAlert).not.toHaveBeenCalled();
});
it('calls refetch and properly sets project.isDeleting to false when the promise resolves', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
expect(MOCK_PROJECT.actionLoadingStates[ACTION_DELETE]).toBe(false);
expect(wrapper.vm.$apollo.queries.projects.refetch).toHaveBeenCalled();
});
});
describe('when API call is not successful', () => {
const error = new Error();
beforeEach(async () => {
deleteProject.mockRejectedValue(error);
createComponent();
jest.spyOn(wrapper.vm.$apollo.queries.projects, 'refetch');
await waitForPromises();
});
it('calls deleteProject and properly sets project.isDeleting to true before the promise resolves', () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
expect(deleteProject).toHaveBeenCalledWith(MOCK_PROJECT.id);
expect(MOCK_PROJECT.actionLoadingStates[ACTION_DELETE]).toBe(true);
});
it('does call createAlert', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred deleting the project. Please refresh the page to try again.',
error,
captureError: true,
});
});
it('calls refetch and properly sets project.isDeleting to false when the promise resolves', async () => {
findProjectsList().vm.$emit('delete', MOCK_PROJECT);
await waitForPromises();
expect(MOCK_PROJECT.actionLoadingStates[ACTION_DELETE]).toBe(false);
expect(wrapper.vm.$apollo.queries.projects.refetch).toHaveBeenCalled();
});
});
});
}); });

View File

@ -4,7 +4,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { organizationProjects, organizationGroups } from '~/organizations/mock_data'; import { organizationProjects, organizationGroups } from '~/organizations/mock_data';
describe('formatProjects', () => { describe('formatProjects', () => {
it('correctly formats the projects', () => { it('correctly formats the projects with delete permissions', () => {
const [firstMockProject] = organizationProjects; const [firstMockProject] = organizationProjects;
const formattedProjects = formatProjects(organizationProjects); const formattedProjects = formatProjects(organizationProjects);
const [firstFormattedProject] = formattedProjects; const [firstFormattedProject] = formattedProjects;
@ -16,7 +16,31 @@ describe('formatProjects', () => {
issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue, issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue,
forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue, forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue,
availableActions: [ACTION_EDIT, ACTION_DELETE], availableActions: [ACTION_EDIT, ACTION_DELETE],
actionLoadingStates: {
[ACTION_DELETE]: false,
},
}); });
expect(formattedProjects.length).toBe(organizationProjects.length);
});
it('correctly formats the projects without delete permissions', () => {
const nonDeletableProject = organizationProjects[organizationProjects.length - 1];
const formattedProjects = formatProjects(organizationProjects);
const nonDeletableFormattedProject = formattedProjects[formattedProjects.length - 1];
expect(nonDeletableFormattedProject).toMatchObject({
id: getIdFromGraphQLId(nonDeletableProject.id),
name: nonDeletableProject.nameWithNamespace,
mergeRequestsAccessLevel: nonDeletableProject.mergeRequestsAccessLevel.stringValue,
issuesAccessLevel: nonDeletableProject.issuesAccessLevel.stringValue,
forkingAccessLevel: nonDeletableProject.forkingAccessLevel.stringValue,
availableActions: [ACTION_EDIT],
actionLoadingStates: {
[ACTION_DELETE]: false,
},
});
expect(formattedProjects.length).toBe(organizationProjects.length); expect(formattedProjects.length).toBe(organizationProjects.length);
}); });
}); });

View File

@ -1,4 +1,5 @@
import { GlFormInput, GlModal, GlAlert } from '@gitlab/ui'; import { GlFormInput, GlModal, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeleteModal from '~/projects/components/shared/delete_modal.vue'; import DeleteModal from '~/projects/components/shared/delete_modal.vue';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
@ -17,6 +18,7 @@ describe('DeleteModal', () => {
mergeRequestsCount: 2, mergeRequestsCount: 2,
forksCount: 3, forksCount: 3,
starsCount: 4, starsCount: 4,
confirmLoading: false,
}; };
const createComponent = (propsData) => { const createComponent = (propsData) => {
@ -143,10 +145,12 @@ describe('DeleteModal', () => {
}); });
}); });
it('emits `primary` event', () => { it('emits `primary` with .prevent event', () => {
createComponent(); createComponent();
findGlModal().vm.$emit('primary'); findGlModal().vm.$emit('primary', {
preventDefault: jest.fn(),
});
expect(wrapper.emitted('primary')).toEqual([[]]); expect(wrapper.emitted('primary')).toEqual([[]]);
}); });
@ -164,4 +168,17 @@ describe('DeleteModal', () => {
expect(wrapper.findByTestId('modal-footer-slot').exists()).toBe(true); expect(wrapper.findByTestId('modal-footer-slot').exists()).toBe(true);
}); });
it('when confirmLoading switches from true to false, emits `change event`', async () => {
createComponent({ confirmLoading: true });
// setProps is justified here because we are testing the component's
// reactive behavior which constitutes an exception
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
wrapper.setProps({ confirmLoading: false });
await nextTick();
expect(wrapper.emitted('change')).toEqual([[false]]);
});
}); });

View File

@ -57,7 +57,6 @@ describe('Merge Requests Artifacts list app', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
store.dispatch('requestArtifacts'); store.dispatch('requestArtifacts');
return nextTick();
}); });
it('renders a loading icon', () => { it('renders a loading icon', () => {
@ -84,7 +83,6 @@ describe('Merge Requests Artifacts list app', () => {
data: artifacts, data: artifacts,
status: HTTP_STATUS_OK, status: HTTP_STATUS_OK,
}); });
return nextTick();
}); });
it('renders a title with the number of artifacts', () => { it('renders a title with the number of artifacts', () => {
@ -107,12 +105,27 @@ describe('Merge Requests Artifacts list app', () => {
}); });
}); });
describe('with 0 artifacts', () => {
beforeEach(() => {
createComponent();
mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_OK, [], {});
store.dispatch('receiveArtifactsSuccess', {
data: [],
status: HTTP_STATUS_OK,
});
});
it('does not render', () => {
expect(findTitle().exists()).toBe(false);
expect(findButtons().exists()).toBe(false);
});
});
describe('with error', () => { describe('with error', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {}, {}); mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {}, {});
store.dispatch('receiveArtifactsError'); store.dispatch('receiveArtifactsError');
return nextTick();
}); });
it('renders the error state', () => { it('renders the error state', () => {

View File

@ -366,6 +366,7 @@ describe('ProjectsListItem', () => {
project: { project: {
...project, ...project,
availableActions: [ACTION_EDIT, ACTION_DELETE], availableActions: [ACTION_EDIT, ACTION_DELETE],
actionLoadingStates: { [ACTION_DELETE]: false },
isForked: true, isForked: true,
editPath, editPath,
}, },
@ -400,6 +401,7 @@ describe('ProjectsListItem', () => {
issuesCount: '0', issuesCount: '0',
forksCount: '0', forksCount: '0',
starsCount: '0', starsCount: '0',
confirmLoading: false,
}); });
}); });

View File

@ -21,11 +21,11 @@ RSpec.describe Resolvers::Ci::Catalog::Resources::VersionsResolver, feature_cate
end end
context 'when name argument is provided' do context 'when name argument is provided' do
let(:name) { 'v1.0' } let(:name) { '1.0.0' }
it 'returns the version that matches the name' do it 'returns the version that matches the name' do
expect(result.items.size).to eq(1) expect(result.items.size).to eq(1)
expect(result.items.first.name).to eq('v1.0') expect(result.items.first.name).to eq('1.0.0')
end end
context 'when no version matches the name' do context 'when no version matches the name' do

View File

@ -447,7 +447,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
aggregate_failures do aggregate_failures do
expect(pipeline_schedule.description).to eq('Schedule Description') expect(pipeline_schedule.description).to eq('Schedule Description')
expect(pipeline_schedule.ref).to eq('master') expect(pipeline_schedule.ref).to eq('refs/heads/master')
expect(pipeline_schedule.cron).to eq('0 4 * * 0') expect(pipeline_schedule.cron).to eq('0 4 * * 0')
expect(pipeline_schedule.cron_timezone).to eq('UTC') expect(pipeline_schedule.cron_timezone).to eq('UTC')
expect(pipeline_schedule.active).to eq(false) expect(pipeline_schedule.active).to eq(false)

View File

@ -3,10 +3,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Search::Navigation, feature_category: :global_search do RSpec.describe Search::Navigation, feature_category: :global_search do
let(:user) { instance_double(User) } let_it_be(:user) { create(:user) }
let(:project_double) { instance_double(Project) } let(:project_double) { instance_double(Project) }
let(:group_double) { instance_double(Group) }
let(:group) { nil }
let(:options) { {} } let(:options) { {} }
let(:search_navigation) { described_class.new(user: user, project: project, options: options) } let(:search_navigation) { described_class.new(user: user, project: project, group: group, options: options) }
describe '#tab_enabled_for_project?' do describe '#tab_enabled_for_project?' do
let(:project) { project_double } let(:project) { project_double }
@ -72,22 +75,19 @@ RSpec.describe Search::Navigation, feature_category: :global_search do
end end
context 'for code tab' do context 'for code tab' do
where(:feature_flag_enabled, :show_elasticsearch_tabs, :project, :tab_enabled, :condition) do where(:project, :group, :tab_enabled_for_project, :condition) do
false | false | nil | false | false nil | nil | false | false
true | true | nil | true | true nil | ref(:group_double) | false | false
true | false | nil | false | false ref(:project_double) | nil | true | true
false | true | nil | false | false ref(:project_double) | nil | false | false
false | false | ref(:project_double) | true | true
true | false | ref(:project_double) | false | false
end end
with_them do with_them do
let(:options) { { show_elasticsearch_tabs: show_elasticsearch_tabs } } let(:options) { {} }
it 'data item condition is set correctly' do it 'data item condition is set correctly' do
allow(search_navigation).to receive(:feature_flag_tab_enabled?) allow(search_navigation).to receive(:tab_enabled_for_project?)
.with(:global_search_code_tab).and_return(feature_flag_enabled) .with(:blobs).and_return(tab_enabled_for_project)
allow(search_navigation).to receive(:tab_enabled_for_project?).with(:blobs).and_return(tab_enabled)
expect(tabs[:blobs][:condition]).to eq(condition) expect(tabs[:blobs][:condition]).to eq(condition)
end end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe SelfHostedSentNotificationsCleanup, feature_category: :database do
after do
connection = described_class.new.connection
connection.execute('ALTER TABLE sent_notifications DROP COLUMN IF EXISTS id_convert_to_bigint')
end
describe '#up' do
context 'when is GitLab.com, dev, or test' do
before do
connection = described_class.new.connection
connection.execute('ALTER TABLE sent_notifications DROP COLUMN IF EXISTS id_convert_to_bigint')
end
it 'does nothing' do
# rubocop: disable RSpec/AnyInstanceOf -- This is the easiest way to test this method
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
# rubocop: enable RSpec/AnyInstanceOf
sent_notifications = table(:sent_notifications)
disable_migrations_output do
reversible_migration do |migration|
migration.before -> {
sent_notifications.reset_column_information
expect(sent_notifications.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
}
migration.after -> {
sent_notifications.reset_column_information
expect(sent_notifications.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
}
end
end
end
end
context 'when is a self-host customer with the temporary column already dropped' do
before do
connection = described_class.new.connection
connection.execute('ALTER TABLE sent_notifications ALTER COLUMN id TYPE bigint')
connection.execute('ALTER TABLE sent_notifications DROP COLUMN IF EXISTS id_convert_to_bigint')
end
it 'does nothing' do
# rubocop: disable RSpec/AnyInstanceOf -- This is the easiest way to test this method
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
# rubocop: enable RSpec/AnyInstanceOf
sent_notifications = table(:sent_notifications)
disable_migrations_output do
migrate!
end
expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
expect(sent_notifications.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
end
end
context 'when is a self-host with the temporary columns' do
before do
connection = described_class.new.connection
connection.execute('ALTER TABLE sent_notifications ALTER COLUMN id TYPE bigint')
connection.execute('ALTER TABLE sent_notifications ADD COLUMN IF NOT EXISTS id_convert_to_bigint integer')
end
it 'drops the temporary columns' do
# rubocop: disable RSpec/AnyInstanceOf -- This is the easiest way to test this method
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
# rubocop: enable RSpec/AnyInstanceOf
sent_notifications = table(:sent_notifications)
disable_migrations_output do
sent_notifications.reset_column_information
expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
expect(sent_notifications.columns.find do |c|
c.name == 'id_convert_to_bigint'
end.sql_type).to eq('integer')
migrate!
sent_notifications.reset_column_information
expect(sent_notifications.columns.find { |c| c.name == 'id' }.sql_type).to eq('bigint')
expect(sent_notifications.columns.find { |c| c.name == 'id_convert_to_bigint' }).to be nil
end
end
end
end
end

View File

@ -6,12 +6,21 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:namespace) { create(:group) } let_it_be(:namespace) { create(:group) }
let_it_be(:public_namespace_project) do let_it_be(:public_namespace_project) do
create(:project, :public, namespace: namespace, name: 'A public namespace project') create(:project, :public, namespace: namespace, name: 'A public namespace project', star_count: 10)
end
let_it_be(:public_project) do
create(:project, :public, name: 'B public test project', star_count: 20)
end
let_it_be(:namespace_project_a) do
create(:project, namespace: namespace, name: 'Test namespace project', star_count: 30)
end
let_it_be(:namespace_project_b) do
create(:project, namespace: namespace, name: 'X namespace Project', star_count: 40)
end end
let_it_be(:public_project) { create(:project, :public, name: 'B public test project') }
let_it_be(:namespace_project_a) { create(:project, namespace: namespace, name: 'Test namespace project') }
let_it_be(:namespace_project_b) { create(:project, namespace: namespace, name: 'X namespace Project') }
let_it_be(:project_noaccess) { create(:project, namespace: namespace, name: 'Project with no access') } let_it_be(:project_noaccess) { create(:project, namespace: namespace, name: 'Project with no access') }
let_it_be(:internal_project) { create(:project, :internal, name: 'Internal project') } let_it_be(:internal_project) { create(:project, :internal, name: 'Internal project') }
@ -94,6 +103,14 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
internal_resource.update!(created_at: tomorrow + 1) internal_resource.update!(created_at: tomorrow + 1)
end end
context 'when there is no sort parameter' do
let_it_be(:sort) { nil }
it 'contains catalog resource sorted by star_count descending' do
is_expected.to eq([private_namespace_resource, public_resource_b, public_resource_a, internal_resource])
end
end
context 'when the sort is created_at ascending' do context 'when the sort is created_at ascending' do
let_it_be(:sort) { :created_at_asc } let_it_be(:sort) { :created_at_asc }

View File

@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:project_a) { create(:project, name: 'A') } let_it_be(:project_a) { create(:project, name: 'A', star_count: 20) }
let_it_be(:project_b) { create(:project, name: 'B') } let_it_be(:project_b) { create(:project, name: 'B', star_count: 10) }
let_it_be(:project_c) { create(:project, name: 'C', description: 'B') } let_it_be(:project_c) { create(:project, name: 'C', description: 'B', star_count: 30) }
let_it_be_with_reload(:resource_a) do let_it_be_with_reload(:resource_a) do
create(:ci_catalog_resource, project: project_a, latest_released_at: '2023-02-01T00:00:00Z') create(:ci_catalog_resource, project: project_a, latest_released_at: '2023-02-01T00:00:00Z')
@ -122,6 +122,22 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do
end end
end end
describe 'order_by_star_count_desc' do
it 'returns catalog resources sorted by project star count in descending order' do
ordered_resources = described_class.order_by_star_count(:desc)
expect(ordered_resources).to eq([resource_c, resource_a, resource_b])
end
end
describe 'order_by_star_count_asc' do
it 'returns catalog resources sorted by project star count in ascending order' do
ordered_resources = described_class.order_by_star_count(:asc)
expect(ordered_resources).to eq([resource_b, resource_a, resource_c])
end
end
describe 'authorized catalog resources' do describe 'authorized catalog resources' do
let_it_be(:namespace) { create(:group) } let_it_be(:namespace) { create(:group) }
let_it_be(:other_namespace) { create(:group) } let_it_be(:other_namespace) { create(:group) }

View File

@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: :pipeline_composition do RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: :pipeline_composition do
using RSpec::Parameterized::TableSyntax
include_context 'when there are catalog resources with versions' include_context 'when there are catalog resources with versions'
it { is_expected.to belong_to(:release) } it { is_expected.to belong_to(:release) }
@ -17,6 +19,27 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category:
it { is_expected.to validate_presence_of(:release) } it { is_expected.to validate_presence_of(:release) }
it { is_expected.to validate_presence_of(:catalog_resource) } it { is_expected.to validate_presence_of(:catalog_resource) }
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
describe 'semver validation' do
where(:version, :valid, :semver_major, :semver_minor, :semver_patch, :semver_prerelease) do
'1' | false | nil | nil | nil | nil
'1.2' | false | nil | nil | nil | nil
'1.2.3' | true | 1 | 2 | 3 | nil
'1.2.3-beta' | true | 1 | 2 | 3 | 'beta'
'1.2.3.beta' | false | nil | nil | nil | nil
end
with_them do
let(:catalog_version) { build(:ci_catalog_resource_version, version: version) }
it do
expect(catalog_version.semver_major).to be semver_major
expect(catalog_version.semver_minor).to be semver_minor
expect(catalog_version.semver_patch).to be semver_patch
expect(catalog_version.semver_prerelease).to eq semver_prerelease
end
end
end
end end
describe '.for_catalog resources' do describe '.for_catalog resources' do
@ -29,10 +52,10 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category:
describe '.by_name' do describe '.by_name' do
it 'returns the version that matches the name' do it 'returns the version that matches the name' do
versions = described_class.by_name('v1.0') versions = described_class.by_name('1.0.0')
expect(versions.count).to eq(1) expect(versions.count).to eq(1)
expect(versions.first.name).to eq('v1.0') expect(versions.first.name).to eq('1.0.0')
end end
context 'when no version matches the name' do context 'when no version matches the name' do
@ -144,8 +167,8 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category:
describe '#readme' do describe '#readme' do
it 'returns the correct readme for the version' do it 'returns the correct readme for the version' do
expect(v1_0.readme.data).to include('Readme v1.0') expect(v1_0.readme.data).to include('Readme 1.0.0')
expect(v1_1.readme.data).to include('Readme v1.1') expect(v1_1.readme.data).to include('Readme 1.1.0')
end end
end end

View File

@ -3,9 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration do RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration do
let_it_be_with_reload(:project) { create_default(:project) } let_it_be_with_reload(:project) { create_default(:project, :repository) }
let_it_be(:repository) { project.repository }
subject { build(:ci_pipeline_schedule) } subject { build(:ci_pipeline_schedule, project: project) }
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:owner) } it { is_expected.to belong_to(:owner) }
@ -25,31 +26,150 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
it_behaves_like 'cleanup by a loose foreign key' do it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:user) } let!(:parent) { create(:user) }
let!(:model) { create(:ci_pipeline_schedule, owner: parent) } let!(:model) { create(:ci_pipeline_schedule, owner: parent, project: project) }
end end
describe 'validations' do describe 'validations' do
it 'does not allow invalid cron patterns' do it 'does not allow invalid cron patterns' do
pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *') pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *', project: project)
expect(pipeline_schedule).not_to be_valid expect(pipeline_schedule).not_to be_valid
end end
it 'does not allow invalid cron patterns' do it 'does not allow invalid cron patterns' do
pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid') pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid', project: project)
expect(pipeline_schedule).not_to be_valid expect(pipeline_schedule).not_to be_valid
end end
it 'does not allow empty variable key' do it 'does not allow empty variable key' do
pipeline_schedule = build(:ci_pipeline_schedule, variables_attributes: [{ secret_value: 'test_value' }]) pipeline_schedule = build(:ci_pipeline_schedule,
variables_attributes: [{ secret_value: 'test_value' }],
project: project)
expect(pipeline_schedule).not_to be_valid expect(pipeline_schedule).not_to be_valid
end end
context 'ref is invalid' do
let_it_be(:ref) { 'ambiguous' }
before_all do
repository.add_tag(project.creator, ref, 'master')
repository.add_branch(project.creator, ref, 'master')
end
context 'when an short ref record is being updated' do
let(:new_description) { 'some description' }
let(:ref) { 'other' }
let(:pipeline_schedule) do
build(:ci_pipeline_schedule, cron: ' 0 0 * * * ', ref: ref, project: project)
end
before do
repository.add_branch(project.creator, ref, 'master')
pipeline_schedule.save!(validate: false)
end
it 'updates the ref' do
pipeline_schedule.update!(description: new_description)
expect(pipeline_schedule.reload.ref).to eq("#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref}")
expect(pipeline_schedule.description).to eq(new_description)
end
context 'when an existing record has no ref' do
let(:pipeline_schedule) do
build(:ci_pipeline_schedule,
cron: ' 0 0 * * * ',
ref: nil,
project: project,
importing: true)
end
it 'updates the record' do
pipeline_schedule.update!(description: new_description)
expect(pipeline_schedule.reload.description).to eq(new_description)
end
end
end
context 'ref is branch and tag' do
let(:pipeline_schedule) { build(:ci_pipeline_schedule, ref: ref, project: project) }
it 'does not allow ambiguous ref' do
pipeline_schedule.valid?
expect(pipeline_schedule.errors.full_messages)
.to include("Ref is ambiguous")
end
context 'importing is enabled' do
let(:pipeline_schedule) do
build(:ci_pipeline_schedule, ref: ref, project: project, importing: true)
end
it 'does not validate the ref' do
expect(pipeline_schedule)
.to be_valid
end
end
end
context 'ref is not a branch or tag' do
let(:ref) { 'unknown' }
let(:pipeline_schedule) { build(:ci_pipeline_schedule, ref: ref, project: project) }
it 'does not allow wrong ref' do
pipeline_schedule.valid?
expect(pipeline_schedule.errors.full_messages)
.to include("Ref is ambiguous")
end
context 'importing is enabled' do
let(:pipeline_schedule) do
build(:ci_pipeline_schedule, ref: ref, project: project, importing: true)
end
it 'does not validate the ref' do
expect(pipeline_schedule)
.to be_valid
end
end
end
end
context 'when an existing record has a valid ref' do
let(:new_description) { 'some description' }
let(:pipeline_schedule) do
build(:ci_pipeline_schedule, cron: ' 0 0 * * * ', project: project)
end
it 'updates the record' do
pipeline_schedule.update!(description: new_description)
expect(pipeline_schedule.reload.description).to eq(new_description)
end
end
context 'when a record is being created' do
let(:ref) { 'master' }
let(:pipeline_schedule) do
build(:ci_pipeline_schedule, cron: ' 0 0 * * * ', project: project, ref: ref)
end
before do
repository.add_branch(project.creator, ref, ref)
end
it 'expands the ref' do
pipeline_schedule.save!
expect(pipeline_schedule.ref).to eq("#{Gitlab::Git::BRANCH_REF_PREFIX}#{ref}")
end
end
context 'when active is false' do context 'when active is false' do
it 'does not allow nullified ref' do it 'does not allow nullified ref' do
pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil) pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil, project: project)
expect(pipeline_schedule).not_to be_valid expect(pipeline_schedule).not_to be_valid
end end
@ -57,7 +177,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
context 'when cron contains trailing whitespaces' do context 'when cron contains trailing whitespaces' do
it 'strips the attribute' do it 'strips the attribute' do
pipeline_schedule = build(:ci_pipeline_schedule, cron: ' 0 0 * * * ') pipeline_schedule = build(:ci_pipeline_schedule, cron: ' 0 0 * * * ', project: project)
expect(pipeline_schedule).to be_valid expect(pipeline_schedule).to be_valid
expect(pipeline_schedule.cron).to eq('0 0 * * *') expect(pipeline_schedule.cron).to eq('0 0 * * *')
@ -70,7 +190,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
let!(:pipeline_schedule) do let!(:pipeline_schedule) do
travel_to(1.day.ago) do travel_to(1.day.ago) do
create(:ci_pipeline_schedule, :hourly) create(:ci_pipeline_schedule, :hourly, project: project)
end end
end end
@ -91,7 +211,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
subject { described_class.preloaded } subject { described_class.preloaded }
before do before do
create_list(:ci_pipeline_schedule, 3) create_list(:ci_pipeline_schedule, 3, project: project)
end end
it 'preloads the associations' do it 'preloads the associations' do
@ -105,8 +225,8 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
describe '.owned_by' do describe '.owned_by' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:owned_pipeline_schedule) { create(:ci_pipeline_schedule, owner: user) } let!(:owned_pipeline_schedule) { create(:ci_pipeline_schedule, owner: user, project: project) }
let!(:other_pipeline_schedule) { create(:ci_pipeline_schedule) } let!(:other_pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
subject { described_class.owned_by(user) } subject { described_class.owned_by(user) }
@ -116,7 +236,6 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
end end
describe '.for_project' do describe '.for_project' do
let(:project) { create(:project) }
let!(:project_pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } let!(:project_pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
let!(:other_pipeline_schedule) { create(:ci_pipeline_schedule) } let!(:other_pipeline_schedule) { create(:ci_pipeline_schedule) }
@ -129,7 +248,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
describe '#set_next_run_at' do describe '#set_next_run_at' do
let(:now) { Time.zone.local(2021, 3, 2, 1, 0) } let(:now) { Time.zone.local(2021, 3, 2, 1, 0) }
let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: "0 1 * * *") } let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: "0 1 * * *", project: project) }
it 'calls fallback method next_run_at if there is no plan limit' do it 'calls fallback method next_run_at if there is no plan limit' do
allow(Settings).to receive(:cron_jobs).and_return({ 'pipeline_schedule_worker' => { 'cron' => "0 1 2 3 *" } }) allow(Settings).to receive(:cron_jobs).and_return({ 'pipeline_schedule_worker' => { 'cron' => "0 1 2 3 *" } })
@ -144,8 +263,13 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
end end
context 'when there are two different pipeline schedules in different time zones' do context 'when there are two different pipeline schedules in different time zones' do
let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') } let(:pipeline_schedule_1) do
let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') } create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)', project: project)
end
let(:pipeline_schedule_2) do
create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC', project: project)
end
it 'sets different next_run_at' do it 'sets different next_run_at' do
expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at) expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at)
@ -154,7 +278,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
end end
describe '#schedule_next_run!' do describe '#schedule_next_run!' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
before do before do
pipeline_schedule.update_column(:next_run_at, nil) pipeline_schedule.update_column(:next_run_at, nil)
@ -179,7 +303,7 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
end end
describe '#job_variables' do describe '#job_variables' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule) } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
let!(:pipeline_schedule_variables) do let!(:pipeline_schedule_variables) do
create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule) create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule)
@ -289,13 +413,13 @@ RSpec.describe Ci::PipelineSchedule, feature_category: :continuous_integration d
context 'loose foreign key on ci_pipeline_schedules.project_id' do context 'loose foreign key on ci_pipeline_schedules.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) } let_it_be(:parent) { create(:project, :repository) }
let!(:model) { create(:ci_pipeline_schedule, project: parent) } let!(:model) { create(:ci_pipeline_schedule, project: parent) }
end end
end end
describe 'before_destroy' do describe 'before_destroy' do
let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: ' 0 0 * * * ') } let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: ' 0 0 * * * ', project: project) }
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } let_it_be_with_reload(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
it 'nullifys associated pipelines' do it 'nullifys associated pipelines' do

View File

@ -38,4 +38,26 @@ RSpec.describe Ci::PipelineVariable, feature_category: :continuous_integration d
end end
end end
end end
describe 'routing table switch' do
context 'with ff disabled' do
before do
stub_feature_flags(ci_partitioning_use_ci_pipeline_variables_routing_table: false)
end
it 'uses the legacy table' do
expect(described_class.table_name).to eq('ci_pipeline_variables')
end
end
context 'with ff enabled' do
before do
stub_feature_flags(ci_partitioning_use_ci_pipeline_variables_routing_table: true)
end
it 'uses the routing table' do
expect(described_class.table_name).to eq('p_ci_pipeline_variables')
end
end
end
end end

View File

@ -30,4 +30,11 @@ RSpec.describe Integrations::Bugzilla, feature_category: :integrations do
it { is_expected.not_to validate_presence_of(:new_issue_url) } it { is_expected.not_to validate_presence_of(:new_issue_url) }
end end
end end
describe '.attribution_notice' do
it do
expect(described_class.attribution_notice)
.to eq('The Bugzilla logo is a trademark of the Mozilla Foundation in the U.S. and other countries.')
end
end
end end

View File

@ -47,12 +47,13 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
describe 'version' do describe 'version' do
where(:ctx, :version) do where(:ctx, :version) do
'version is blank' | '' 'can\'t be blank' | ''
'version is not valid package version' | '!!()()' 'is invalid' | '!!()()'
'version is too large' | ('a' * 256) 'is too long (maximum is 255 characters)' | ('a' * 256)
'must follow semantic version' | '1'
end end
with_them do with_them do
it { expect(errors).to include(:version) } it { expect(errors.messages.values.flatten).to include(ctx) }
end end
context 'when version is not unique in project+name' do context 'when version is not unique in project+name' do
@ -272,7 +273,7 @@ RSpec.describe Ml::ModelVersion, feature_category: :mlops do
end end
context 'when parsing semver components' do context 'when parsing semver components' do
let(:model_version) { build(:ml_model_versions, model: model1, semver: semver, project: base_project) } let(:model_version) { build(:ml_model_versions, model: model1, version: semver, project: base_project) }
where(:semver, :valid, :major, :minor, :patch, :prerelease) do where(:semver, :valid, :major, :minor, :patch, :prerelease) do
'1' | false | nil | nil | nil | nil '1' | false | nil | nil | nil | nil

View File

@ -327,6 +327,28 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do
end end
end end
describe '.expiring_and_not_notified_without_impersonation' do
let_it_be(:expired_token) { create(:personal_access_token, expires_at: 2.days.ago) }
let_it_be(:revoked_token) { create(:personal_access_token, revoked: true) }
let_it_be(:valid_token_and_notified) { create(:personal_access_token, expires_at: 2.days.from_now, expire_notification_delivered: true) }
let_it_be(:valid_token) { create(:personal_access_token, expires_at: 2.days.from_now, impersonation: false) }
let_it_be(:long_expiry_token) { create(:personal_access_token, expires_at: described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now) }
context 'when token is there to be notified' do
it "has only unnotified tokens" do
expect(described_class.expiring_and_not_notified_without_impersonation).to contain_exactly(valid_token)
end
end
context 'when no token is there to be notified' do
it "return empty array" do
valid_token.update!(impersonation: true)
expect(described_class.expiring_and_not_notified_without_impersonation).to be_empty
end
end
end
describe '.expired_today_and_not_notified' do describe '.expired_today_and_not_notified' do
let_it_be(:active) { create(:personal_access_token) } let_it_be(:active) { create(:personal_access_token) }
let_it_be(:expired_yesterday) { create(:personal_access_token, expires_at: Date.yesterday) } let_it_be(:expired_yesterday) { create(:personal_access_token, expires_at: Date.yesterday) }

View File

@ -162,6 +162,7 @@ RSpec.describe User, feature_category: :user_profile do
it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) } it { is_expected.to have_many(:keys).dependent(:destroy) }
it { is_expected.to have_many(:expired_today_and_unnotified_keys) } it { is_expected.to have_many(:expired_today_and_unnotified_keys) }
it { is_expected.to have_many(:expiring_soon_and_unnotified_personal_access_tokens) }
it { is_expected.to have_many(:deploy_keys).dependent(:nullify) } it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
it { is_expected.to have_many(:group_deploy_keys) } it { is_expected.to have_many(:group_deploy_keys) }
it { is_expected.to have_many(:events).dependent(:delete_all) } it { is_expected.to have_many(:events).dependent(:delete_all) }
@ -1413,6 +1414,24 @@ RSpec.describe User, feature_category: :user_profile do
end end
end end
describe '.with_personal_access_tokens_expiring_soon_and_ids' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:pat1) { create(:personal_access_token, user: user1, expires_at: 2.days.from_now) }
let_it_be(:pat2) { create(:personal_access_token, user: user2, expires_at: 7.days.from_now) }
let_it_be(:ids) { [user1.id] }
subject(:users) { described_class.with_personal_access_tokens_expiring_soon_and_ids(ids) }
it 'filters users only by id' do
expect(users).to contain_exactly(user1)
end
it 'includes expiring personal access tokens' do
expect(users.first.expiring_soon_and_unnotified_personal_access_tokens).to be_loaded
end
end
describe '.active_without_ghosts' do describe '.active_without_ghosts' do
let_it_be(:user1) { create(:user, :external) } let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') } let_it_be(:user2) { create(:user, state: 'blocked') }

View File

@ -318,6 +318,7 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :fleet_v
expect(json_response['status']).to eq('never_contacted') expect(json_response['status']).to eq('never_contacted')
expect(json_response['active']).to eq(true) expect(json_response['active']).to eq(true)
expect(json_response['paused']).to eq(false) expect(json_response['paused']).to eq(false)
expect(json_response['maintenance_note']).to be_nil
end end
end end
@ -486,6 +487,14 @@ RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :fleet_v
expect(shared_runner.reload.maximum_timeout).to eq(1234) expect(shared_runner.reload.maximum_timeout).to eq(1234)
end end
it 'maintenance note' do
maintenance_note = shared_runner.maintenance_note
update_runner(shared_runner.id, admin, maintenance_note: "#{maintenance_note}_updated")
expect(response).to have_gitlab_http_status(:ok)
expect(shared_runner.reload.maintenance_note).to eq("#{maintenance_note}_updated")
end
it 'fails with no parameters' do it 'fails with no parameters' do
put api("/runners/#{shared_runner.id}", admin) put api("/runners/#{shared_runner.id}", admin)

View File

@ -45,7 +45,7 @@ RSpec.describe 'PipelineSchedulecreate', feature_category: :continuous_integrati
description: 'created_desc', description: 'created_desc',
cron: '0 1 * * *', cron: '0 1 * * *',
cronTimezone: 'UTC', cronTimezone: 'UTC',
ref: 'patch-x', ref: 'master',
active: true, active: true,
variables: [ variables: [
{ key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' } { key: 'AAA', value: "AAA123", variableType: 'ENV_VAR' }
@ -107,7 +107,11 @@ RSpec.describe 'PipelineSchedulecreate', feature_category: :continuous_integrati
expect(mutation_response['errors']) expect(mutation_response['errors'])
.to match_array( .to match_array(
["Cron is invalid syntax", "Cron timezone is invalid syntax"] [
"Cron is invalid syntax",
"Cron timezone is invalid syntax",
"Ref is ambiguous"
]
) )
end end
end end

View File

@ -17,6 +17,7 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule) create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule)
end end
let(:repository) { project.repository }
let(:mutation) do let(:mutation) do
variables = { variables = {
id: pipeline_schedule.to_global_id.to_s, id: pipeline_schedule.to_global_id.to_s,
@ -73,6 +74,10 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
} }
end end
before do
repository.add_branch(project.creator, 'patch-x', 'master')
end
it do it do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
@ -146,7 +151,8 @@ RSpec.describe 'PipelineScheduleUpdate', feature_category: :continuous_integrati
"Cron is invalid syntax", "Cron is invalid syntax",
"Cron timezone is invalid syntax", "Cron timezone is invalid syntax",
"Ref can't be blank", "Ref can't be blank",
"Description can't be blank" "Description can't be blank",
"Ref is ambiguous"
] ]
) )
end end

View File

@ -8,7 +8,7 @@ RSpec.describe Ci::Catalog::Resources::ReleaseService, feature_category: :pipeli
it 'validates the catalog resource and creates a version' do it 'validates the catalog resource and creates a version' do
project = create(:project, :catalog_resource_with_components) project = create(:project, :catalog_resource_with_components)
catalog_resource = create(:ci_catalog_resource, project: project) catalog_resource = create(:ci_catalog_resource, project: project)
release = create(:release, project: project, sha: project.repository.root_ref_sha) release = create(:release, project: project, sha: project.repository.root_ref_sha, tag: '1.0.0')
response = described_class.new(release).execute response = described_class.new(release).execute

View File

@ -24,13 +24,13 @@ RSpec.describe Ci::Catalog::Resources::Versions::CreateService, feature_category
) )
end end
let(:release) { create(:release, project: project, sha: project.repository.root_ref_sha) } let(:release) { create(:release, tag: '1.2.0', project: project, sha: project.repository.root_ref_sha) }
let!(:catalog_resource) { create(:ci_catalog_resource, project: project) } let!(:catalog_resource) { create(:ci_catalog_resource, project: project) }
context 'when the project is not a catalog resource' do context 'when the project is not a catalog resource' do
it 'does not create a version' do it 'does not create a version' do
project = create(:project, :repository) project = create(:project, :repository)
release = create(:release, project: project, sha: project.repository.root_ref_sha) release = create(:release, tag: '1.2.1', project: project, sha: project.repository.root_ref_sha)
response = described_class.new(release).execute response = described_class.new(release).execute

View File

@ -6,6 +6,7 @@ RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuo
let_it_be(:reporter) { create(:user) } let_it_be(:reporter) { create(:user) }
let_it_be_with_reload(:user) { create(:user) } let_it_be_with_reload(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :public, :repository) } let_it_be_with_reload(:project) { create(:project, :public, :repository) }
let_it_be_with_reload(:repository) { project.repository }
subject(:service) { described_class.new(project, user, params) } subject(:service) { described_class.new(project, user, params) }
@ -15,6 +16,10 @@ RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuo
end end
describe "execute" do describe "execute" do
before_all do
repository.add_branch(project.creator, 'patch-x', 'master')
end
context 'when user does not have permission' do context 'when user does not have permission' do
subject(:service) { described_class.new(project, reporter, {}) } subject(:service) { described_class.new(project, reporter, {}) }
@ -48,7 +53,7 @@ RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuo
expect(result.payload).to have_attributes( expect(result.payload).to have_attributes(
description: 'desc', description: 'desc',
ref: 'patch-x', ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}patch-x",
active: false, active: false,
cron: '*/1 * * * *', cron: '*/1 * * * *',
cron_timezone: 'UTC' cron_timezone: 'UTC'

View File

@ -14,10 +14,13 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule) key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
end end
let_it_be_with_reload(:repository) { project.repository }
before_all do before_all do
project.add_maintainer(user) project.add_maintainer(user)
project.add_owner(project_owner) project.add_owner(project_owner)
project.add_reporter(reporter) project.add_reporter(reporter)
repository.add_branch(project.creator, 'patch-x', 'master')
pipeline_schedule.reload pipeline_schedule.reload
end end
@ -58,7 +61,8 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
service.execute service.execute
pipeline_schedule.reload pipeline_schedule.reload
end.to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc') end.to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc')
.and change { pipeline_schedule.ref }.from('master').to('patch-x') .and change { pipeline_schedule.ref }
.from("#{Gitlab::Git::BRANCH_REF_PREFIX}master").to("#{Gitlab::Git::BRANCH_REF_PREFIX}patch-x")
.and change { pipeline_schedule.active }.from(true).to(false) .and change { pipeline_schedule.active }.from(true).to(false)
.and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *') .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
.and change { pipeline_schedule.variables.last.key }.from('foo').to('bar') .and change { pipeline_schedule.variables.last.key }.from('foo').to('bar')

View File

@ -56,7 +56,7 @@ RSpec.describe Releases::CreateService, feature_category: :continuous_integratio
end end
context 'when project is a catalog resource' do context 'when project is a catalog resource' do
let(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') } let_it_be(:project) { create(:project, :catalog_resource_with_components, create_tag: '6.0.0') }
let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) } let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) }
let(:ref) { 'master' } let(:ref) { 'master' }

View File

@ -6,33 +6,33 @@
RSpec.shared_context 'when there are catalog resources with versions' do RSpec.shared_context 'when there are catalog resources with versions' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:project1) { create(:project, :custom_repo, files: { 'README.md' => 'Readme v1.0' }) } let_it_be(:project1) { create(:project, :custom_repo, files: { 'README.md' => 'Readme 1.0.0' }) }
let_it_be(:project2) { create(:project, :repository) } let_it_be(:project2) { create(:project, :repository) }
let_it_be_with_reload(:resource1) { create(:ci_catalog_resource, project: project1) } let_it_be_with_reload(:resource1) { create(:ci_catalog_resource, project: project1) }
let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) } let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) }
let(:v1_0) { resource1.versions.by_name('v1.0').first } let(:v1_0) { resource1.versions.by_name('1.0.0').first }
let(:v1_1) { resource1.versions.by_name('v1.1').first } let(:v1_1) { resource1.versions.by_name('1.1.0').first }
let(:v2_0) { resource2.versions.by_name('v2.0').first } let(:v2_0) { resource2.versions.by_name('2.0.0').first }
let(:v2_1) { resource2.versions.by_name('v2.1').first } let(:v2_1) { resource2.versions.by_name('2.1.0').first }
before_all do before_all do
project1.repository.create_branch('branch_v1.1', project1.default_branch) project1.repository.create_branch('branch_v1.1', project1.default_branch)
project1.repository.update_file( project1.repository.update_file(
current_user, 'README.md', 'Readme v1.1', message: 'Update readme', branch_name: 'branch_v1.1') current_user, 'README.md', 'Readme 1.1.0', message: 'Update readme', branch_name: 'branch_v1.1')
tag_v1_0 = project1.repository.add_tag(current_user, 'v1.0', project1.default_branch) tag_v1_0 = project1.repository.add_tag(current_user, '1.0.0', project1.default_branch)
tag_v1_1 = project1.repository.add_tag(current_user, 'v1.1', 'branch_v1.1') tag_v1_1 = project1.repository.add_tag(current_user, '1.1.0', 'branch_v1.1')
release_v1_0 = create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago, release_v1_0 = create(:release, project: project1, tag: '1.0.0', released_at: 4.days.ago,
sha: tag_v1_0.dereferenced_target.sha) sha: tag_v1_0.dereferenced_target.sha)
release_v1_1 = create(:release, project: project1, tag: 'v1.1', released_at: 3.days.ago, release_v1_1 = create(:release, project: project1, tag: '1.1.0', released_at: 3.days.ago,
sha: tag_v1_1.dereferenced_target.sha) sha: tag_v1_1.dereferenced_target.sha)
release_v2_0 = create(:release, project: project2, tag: 'v2.0', released_at: 2.days.ago) release_v2_0 = create(:release, project: project2, tag: '2.0.0', released_at: 2.days.ago)
release_v2_1 = create(:release, project: project2, tag: 'v2.1', released_at: 1.day.ago) release_v2_1 = create(:release, project: project2, tag: '2.1.0', released_at: 1.day.ago)
create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_0, created_at: 1.day.ago) create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_0, created_at: 1.day.ago)
create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_1, created_at: 2.days.ago) create(:ci_catalog_resource_version, catalog_resource: resource1, release: release_v1_1, created_at: 2.days.ago)

View File

@ -19,7 +19,7 @@ RSpec.shared_examples 'pipeline schedules checking variables permission' do
expect(result.status).to eq(:success) expect(result.status).to eq(:success)
expect(result.payload).to have_attributes( expect(result.payload).to have_attributes(
description: 'desc', description: 'desc',
ref: 'patch-x', ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}patch-x",
active: false, active: false,
cron: '*/1 * * * *', cron: '*/1 * * * *',
cron_timezone: 'UTC' cron_timezone: 'UTC'

View File

@ -36,7 +36,8 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_cate
create(:personal_access_token, user: user2, expires_at: 5.days.from_now) create(:personal_access_token, user: user2, expires_at: 5.days.from_now)
# Query count increased for the user look up # Query count increased for the user look up
expect { worker.perform }.not_to exceed_all_query_limit(control).with_threshold(4) # there are still 2 N+1 queries one for token name look up and another for token update.
expect { worker.perform }.not_to exceed_all_query_limit(control).with_threshold(2)
end end
end end

View File

@ -9,7 +9,7 @@ RSpec.describe RunPipelineScheduleWorker, feature_category: :continuous_integrat
describe '#perform' do describe '#perform' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) } let_it_be(:project) { create(:project, :repository, namespace: group) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) } let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) }

View File

@ -1321,10 +1321,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.83.0.tgz#5d6799e5fe3fb564b7e4190d90876469bd1608ba" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.83.0.tgz#5d6799e5fe3fb564b7e4190d90876469bd1608ba"
integrity sha512-881f6OsxREgBXYn9fkg+XGweBFbrGdrssrIzFIZFSG95GF/K+HILw1mXZ9nq7C5Xb5JDWPKJGYnKuHw5vvWm5Q== integrity sha512-881f6OsxREgBXYn9fkg+XGweBFbrGdrssrIzFIZFSG95GF/K+HILw1mXZ9nq7C5Xb5JDWPKJGYnKuHw5vvWm5Q==
"@gitlab/ui@^74.6.0": "@gitlab/ui@^74.7.0":
version "74.6.0" version "74.7.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-74.6.0.tgz#187b76e9e153365ce15a3d8d7bb6eae7f5209978" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-74.7.0.tgz#58493e867cfa0fc6e8ea035dc7e89f7a3748bc24"
integrity sha512-a9hTrqg+1wbZYrNZs0AX1F6mPGRdPBhh6pebzP/KAGRUqlQ5o6mOE8WVBbEy2DCMhEAnvmFSdramP4aD4WKzvg== integrity sha512-IXDyfv/Jb0bFBJOaAOl1cb4pI4WXucPi74hcIBYaWcDerBKhrGlcJwA0mvx3VcesFa8s2mwQUNcZ1FAXiUwqFA==
dependencies: dependencies:
"@floating-ui/dom" "1.4.3" "@floating-ui/dom" "1.4.3"
bootstrap-vue "2.23.1" bootstrap-vue "2.23.1"