Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-12-13 00:15:40 +00:00
parent 42a4fe5b39
commit 95e5fa3fb3
39 changed files with 781 additions and 155 deletions

View File

@ -1231,6 +1231,7 @@ lib/gitlab/checks/**
/lib/tasks/gitlab/password.rake
/lib/tasks/tokens.rake
# Necessary for GitLab availability
[Verify] @gitlab-org/maintainers/cicd-verify @shinya.maeda @stanhu @ayufan
# With these catch-all rules we will require backend approval and use it as an
# opportunity to refine specific rules defined in this section.
@ -1382,6 +1383,7 @@ lib/gitlab/checks/**
/lib/gitlab/ci/templates/Jobs/SAST.*.yml @gitlab-org/secure/static-analysis
/lib/gitlab/ci/templates/Jobs/Secret-Detection.*.yml @gitlab-org/secure/static-analysis
# Require approvals for Project API entities affecting availability
[Data Stores::Tenant Scale] @abdwdd @alexpooley @manojmj
lib/api/entities/basic_project_details.rb
lib/api/entities/project_with_access.rb
@ -1440,17 +1442,18 @@ ee/lib/ee/api/entities/project.rb
/ee/app/assets/javascripts/usage_quotas/seats/
/ee/app/assets/javascripts/usage_quotas/storage/
[Manage::Foundations] @gitlab-org/manage/foundations/engineering
^[Manage::Foundations] @gitlab-org/manage/foundations/engineering
/lib/sidebars/
/ee/lib/sidebars/
/ee/lib/ee/sidebars/
# Necessary for availablity, similar to DB migrations
[Global Search] @gitlab-org/search-team/migration-maintainers
/ee/elastic/migrate/
/ee/spec/elastic/migrate/
/ee/spec/support/elastic.rb
[Create::IDE - Remote Development Backend] @gitlab-org/maintainers/remote-development/backend
^[Create::IDE - Remote Development Backend] @gitlab-org/maintainers/remote-development/backend
/ee/app/models/remote_development/
/ee/app/policies/remote_development/
/ee/app/finders/remote_development/
@ -1476,14 +1479,14 @@ ee/lib/ee/api/entities/project.rb
/ee/spec/services/remote_development/
/qa/qa/specs/features/**/remote_development/ @gitlab-org/maintainers/remote-development/backend @gl-quality/qe-maintainers
[Create::IDE - Remote Development Frontend] @gitlab-org/maintainers/remote-development/frontend
^[Create::IDE - Remote Development Frontend] @gitlab-org/maintainers/remote-development/frontend
/ee/app/assets/remote_development/
/ee/app/assets/**/remote_development/
/ee/app/views/remote_development/
/ee/spec/frontend/remote_development/
/ee/spec/frontend/**/remote_development/
[Govern::Anti-abuse] @gitlab-org/modelops/anti-abuse
^[Govern::Anti-abuse] @gitlab-org/modelops/anti-abuse
/ee/app/controllers/users/identity_verification_controller.rb
/ee/app/models/concerns/identity_verifiable.rb
/ee/config/routes/identity_verification.rb

View File

@ -88,31 +88,43 @@ linters:
# These cops are incredibly noisy when it comes to HAML templates, so we
# ignore them.
- Layout/BlockAlignment
- Layout/EndAlignment
- Layout/HashAlignment
- Layout/IndentationConsistency
- Layout/IndentationWidth
- Layout/LineLength
- Layout/TrailingWhitespace
- Lint/Void
- Naming/FileName
- Style/AlignParameters
- Style/BlockNesting
- Style/ElseAlignment
- Style/FileName
- Style/FinalNewline
- Layout/ElseAlignment
- Style/FrozenStringLiteralComment
- Style/IfUnlessModifier
- Style/IndentationWidth
- Style/Next
- Style/SoleNestedConditional
- Style/TrailingWhitespace
- Style/StringLiteralsInInterpolation
- Style/WhileUntilModifier
- Cop/StaticTranslationDefinition
# These cops should eventually get enabled
# haml-lint force enables these: https://github.com/sds/haml-lint/blob/v0.51.0/config/forced_rubocop_config.yml
- Layout/ArgumentAlignment
- Layout/ArrayAlignment
- Layout/EndAlignment
- Cop/LineBreakAfterGuardClauses
- Cop/LineBreakAroundConditionalBlock
- Cop/ProjectPathHelper
- Gitlab/FeatureAvailableUsage
- Gitlab/Json
- GitlabSecurity/PublicSend
- Layout/FirstHashElementIndentation
- Layout/EmptyLineAfterGuardClause
- Layout/EmptyLines
- Layout/EmptyLinesAroundBlockBody
- Layout/ExtraSpacing
- Layout/InitialIndentation
- Layout/LeadingCommentSpace
- Layout/MultilineHashBraceLayout
- Layout/SpaceAroundOperators
- Layout/SpaceBeforeComma
- Layout/SpaceBeforeFirstArg
@ -123,6 +135,7 @@ linters:
- Lint/AssignmentInCondition
- Lint/LiteralInInterpolation
- Lint/ParenthesesAsGroupedExpression
- Lint/RedundantStringCoercion
- Lint/SafeNavigationConsistency
- Lint/SymbolConversion
- Lint/UnusedBlockArgument
@ -140,6 +153,7 @@ linters:
- Style/IdenticalConditionalBranches
- Style/IfInsideElse
- Style/InlineDisableAnnotation
- Style/MultilineTernaryOperator
- Style/NegatedIf
- Style/NestedTernaryOperator
- Style/RedundantInterpolation
@ -148,7 +162,6 @@ linters:
- Style/TernaryParentheses
- Style/TrailingCommaInHashLiteral
- Style/UnlessElse
- Style/UnneededCondition
- Style/WordArray
- Style/ZeroLengthPredicate
@ -178,6 +191,9 @@ linters:
TagName:
enabled: true
TrailingEmptyLines:
enabled: false
TrailingWhitespace:
enabled: true

2
DEI.md
View File

@ -67,7 +67,7 @@ Inclusive leadership is addressed in our project through a variety of different
- One or more project maintainers in the GitLab organization have completed neurodiversity and other DEI training.
- One or more project maintainers in the GitLab organization are a member of a working group related to a DEI initiative.
- One or more project maintainers in the GitLab organization participate in DEI group meetings or events.
- GitLab's hiring managers ensure a diverse candidate slate and interview panel.
- GitLab's hiring managers seek candidates from a diverse range of candidate pools to ensure the most qualified candidate is hired and GitLab's interview panels are representative of GitLab and society's diversity.
Our project recognizes that the inclusion of the DEI.md file and the provided reflection on the specific DEI metrics does not ensure community safety nor community inclusiveness. The inclusion of the DEI.md file signals that we, as a project, are committed to centering DEI in our project and regularly reviewing and reflecting on our project DEI practices.

View File

@ -434,7 +434,7 @@ group :development, :test do
gem 'gitlab-styles', '~> 11.0.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'haml_lint', '~> 0.40.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'haml_lint', '~> 0.51', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'bundler-audit', '~> 0.9.1', require: false # rubocop:todo Gemfile/MissingFeatureCategory
# Benchmarking & profiling

View File

@ -282,7 +282,7 @@
{"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"},
{"name":"guard-rspec","version":"4.7.3","platform":"ruby","checksum":"a47ba03cbd1e3c71e6ae8645cea97e203098a248aede507461a43e906e2f75ca"},
{"name":"haml","version":"5.2.2","platform":"ruby","checksum":"6e759246556145642ef832d670fc06f9bd8539159a0e600847a00291dd7aae0c"},
{"name":"haml_lint","version":"0.40.1","platform":"ruby","checksum":"b658322eb245399e40b19a27a341039c76aead5794bc622d469e877162e34802"},
{"name":"haml_lint","version":"0.51.0","platform":"ruby","checksum":"6c5e73b979dcd806ddf0043971bfc2076f832c24722314503ebb1087c361a8e7"},
{"name":"hamlit","version":"2.15.0","platform":"java","checksum":"fda165464e59337ab7cda6304a66bfdb607bb7155f25566da19c9ee7b98e03d1"},
{"name":"hamlit","version":"2.15.0","platform":"ruby","checksum":"d2e8505362338945fa309c68b2b8be07ebdc181200ec6021223567bf66dac38e"},
{"name":"hana","version":"1.3.7","platform":"ruby","checksum":"5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d"},

View File

@ -872,11 +872,11 @@ GEM
haml (5.2.2)
temple (>= 0.8.0)
tilt
haml_lint (0.40.1)
haml (>= 4.0, < 5.3)
haml_lint (0.51.0)
haml (>= 4.0)
parallel (~> 1.10)
rainbow
rubocop (>= 0.50.0)
rubocop (>= 1.0)
sysexits (~> 1.1)
hamlit (2.15.0)
temple (>= 0.8.2)
@ -1923,7 +1923,7 @@ DEPENDENCIES
grpc (~> 1.58.0)
gssapi (~> 1.3.1)
guard-rspec
haml_lint (~> 0.40.0)
haml_lint (~> 0.51)
hamlit (~> 2.15.0)
hashie (~> 5.0.0)
health_check (~> 3.0)

View File

@ -2,14 +2,14 @@
import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
import * as i18n from '../translations';
export default {
name: 'ShowMlModelApp',
components: {
ModelVersionList,
ModelVersionList: () => import('../components/model_version_list.vue'),
CandidateList: () => import('../components/candidate_list.vue'),
TitleArea,
GlTabs,
GlTab,
@ -74,6 +74,8 @@ export default {
{{ $options.i18n.MODEL_CANDIDATES_TAB_LABEL }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ candidateCount }}</gl-badge>
</template>
<candidate-list :model-id="model.id" />
</gl-tab>
</gl-tabs>
</div>

View File

@ -0,0 +1,139 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { n__ } from '~/locale';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
import { makeLoadCandidatesErrorMessage, NO_CANDIDATES_LABEL } from '../translations';
import getModelCandidatesQuery from '../graphql/queries/get_model_candidates.query.graphql';
import { GRAPHQL_PAGE_SIZE } from '../constants';
export default {
name: 'MlCandidateList',
components: {
GlAlert,
CandidateListRow,
PackagesListLoader,
RegistryList,
},
props: {
modelId: {
type: Number,
required: true,
},
},
data() {
return {
modelVersions: {},
errorMessage: undefined,
};
},
apollo: {
candidates: {
query: getModelCandidatesQuery,
variables() {
return this.queryVariables;
},
update(data) {
return data.mlModel?.candidates ?? {};
},
error(error) {
this.errorMessage = makeLoadCandidatesErrorMessage(error.message);
Sentry.captureException(error);
},
},
},
computed: {
gid() {
return convertToGraphQLId('Ml::Model', this.modelId);
},
isListEmpty() {
return this.count === 0;
},
isLoading() {
return this.$apollo.queries.candidates.loading;
},
pageInfo() {
return this.candidates?.pageInfo ?? {};
},
listTitle() {
return n__('%d candidate', '%d candidates', this.count);
},
queryVariables() {
return {
id: this.gid,
first: GRAPHQL_PAGE_SIZE,
};
},
items() {
return this.candidates?.nodes ?? [];
},
count() {
return this.candidates?.count ?? 0;
},
},
methods: {
fetchPage({ first = null, last = null, before = null, after = null } = {}) {
const variables = {
...this.queryVariables,
first,
last,
before,
after,
};
this.$apollo.queries.candidates.fetchMore({
variables,
updateQuery: (previousResult, { fetchMoreResult }) => {
return fetchMoreResult;
},
});
},
fetchPreviousCandidatesPage() {
this.fetchPage({
last: GRAPHQL_PAGE_SIZE,
before: this.pageInfo?.startCursor,
});
},
fetchNextCandidatesPage() {
this.fetchPage({
first: GRAPHQL_PAGE_SIZE,
after: this.pageInfo?.endCursor,
});
},
},
i18n: {
NO_CANDIDATES_LABEL,
},
};
</script>
<template>
<div>
<div v-if="isLoading">
<packages-list-loader />
</div>
<gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">{{
errorMessage
}}</gl-alert>
<div v-else-if="isListEmpty" class="gl-text-secondary">
{{ $options.i18n.NO_CANDIDATES_LABEL }}
</div>
<div v-else>
<registry-list
:hidden-delete="true"
:is-loading="isLoading"
:items="items"
:pagination="pageInfo"
:title="listTitle"
@prev-page="fetchPreviousCandidatesPage"
@next-page="fetchNextCandidatesPage"
>
<template #default="{ item }">
<candidate-list-row :candidate="item" />
</template>
</registry-list>
</div>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script>
import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'MlCandidateListRow',
components: {
ListItem,
GlLink,
GlTruncate,
GlSprintf,
TimeAgoTooltip,
},
props: {
candidate: {
type: Object,
required: true,
},
},
computed: {
pathToDetails() {
return this.candidate._links?.showPath;
},
},
};
</script>
<template>
<list-item v-bind="$attrs">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center">
<gl-link class="gl-text-body" :href="pathToDetails">
<gl-truncate :text="candidate.name" />
</gl-link>
</div>
</template>
<template #left-secondary>
<span>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<time-ago-tooltip :time="candidate.createdAt" />
</template>
</gl-sprintf>
</span>
</template>
</list-item>
</template>

View File

@ -4,10 +4,7 @@ import { n__ } from '~/locale';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE,
NO_VERSIONS_LABEL,
} from '~/ml/model_registry/translations';
import { makeLoadVersionsErrorMessage, NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import getModelVersionsQuery from '../graphql/queries/get_model_versions.query.graphql';
import { GRAPHQL_PAGE_SIZE } from '../constants';
@ -29,7 +26,7 @@ export default {
data() {
return {
modelVersions: {},
fetchModelVersionsError: false,
errorMessage: undefined,
};
},
apollo: {
@ -42,7 +39,7 @@ export default {
return data.mlModel?.versions ?? {};
},
error(error) {
this.fetchModelVersionsError = true;
this.errorMessage = makeLoadVersionsErrorMessage(error.message);
Sentry.captureException(error);
},
},
@ -108,7 +105,6 @@ export default {
},
},
i18n: {
FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE,
NO_VERSIONS_LABEL,
},
};
@ -118,8 +114,8 @@ export default {
<div v-if="isLoading">
<packages-list-loader />
</div>
<gl-alert v-else-if="fetchModelVersionsError" variant="danger" :dismissible="false">{{
$options.i18n.FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE
<gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">{{
errorMessage
}}</gl-alert>
<div v-else-if="isListEmpty" class="gl-text-secondary">
{{ $options.i18n.NO_VERSIONS_LABEL }}

View File

@ -0,0 +1,28 @@
query getModelCandidates(
$id: MlModelID!
$first: Int
$last: Int
$after: String
$before: String
) {
mlModel(id: $id) {
id
candidates(after: $after, before: $before, first: $first, last: $last) {
count
nodes {
id
name
createdAt
_links {
showPath
}
}
pageInfo {
hasNextPage
hasPreviousPage
endCursor
startCursor
}
}
}
}

View File

@ -1,4 +1,4 @@
import { __, s__, n__ } from '~/locale';
import { __, s__, n__, sprintf } from '~/locale';
export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
@ -34,6 +34,14 @@ export const CI_SECTION_LABEL = s__('MlModelRegistry|CI Info');
export const JOB_LABEL = __('Job');
export const CI_USER_LABEL = s__('MlModelRegistry|Triggered by');
export const CI_MR_LABEL = __('Merge request');
export const FAILED_TO_LOAD_MODEL_VERSIONS_MESSAGE = s__(
'MlModelRegistry|Failed to load model versions',
);
export const makeLoadVersionsErrorMessage = (message) =>
sprintf(s__('MlModelRegistry|Failed to load model versions with error: %{message}'), {
message,
});
export const NO_CANDIDATES_LABEL = s__('MlModelRegistry|This model has no candidates');
export const makeLoadCandidatesErrorMessage = (message) =>
sprintf(s__('MlModelRegistry|Failed to load model candidates with error: %{message}'), {
message,
});

View File

@ -16,6 +16,7 @@ import {
CVE_ID_REQUEST_BUTTON_I18N,
featureAccessLevelDescriptions,
modelExperimentsHelpPath,
modelRegistryHelpPath,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
import ProjectFeatureSetting from './project_feature_setting.vue';
@ -63,6 +64,8 @@ export default {
modelExperimentsHelpText: s__(
'ProjectSettings|Track machine learning model experiments and artifacts.',
),
modelRegistryLabel: s__('ProjectSettings|Model registry'),
modelRegistryHelpText: s__('ProjectSettings|Manage machine learning models.'),
pagesLabel: s__('ProjectSettings|Pages'),
repositoryLabel: s__('ProjectSettings|Repository'),
requirementsLabel: s__('ProjectSettings|Requirements'),
@ -83,7 +86,7 @@ export default {
VISIBILITY_LEVEL_INTERNAL_INTEGER,
VISIBILITY_LEVEL_PUBLIC_INTEGER,
modelExperimentsHelpPath,
modelRegistryHelpPath,
components: {
CiCatalogSettings,
ProjectFeatureSetting,
@ -259,6 +262,7 @@ export default {
mergeRequestsAccessLevel: featureAccessLevel.EVERYONE,
packageRegistryAccessLevel: featureAccessLevel.EVERYONE,
modelExperimentsAccessLevel: featureAccessLevel.EVERYONE,
modelRegistryAccessLevel: featureAccessLevel.EVERYONE,
buildsAccessLevel: featureAccessLevel.EVERYONE,
wikiAccessLevel: featureAccessLevel.EVERYONE,
snippetsAccessLevel: featureAccessLevel.EVERYONE,
@ -411,6 +415,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.modelExperimentsAccessLevel,
);
this.modelRegistryAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.modelRegistryAccessLevel,
);
this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
@ -475,6 +483,8 @@ export default {
this.wikiAccessLevel = featureAccessLevel.EVERYONE;
if (this.modelExperimentsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.modelExperimentsAccessLevel = featureAccessLevel.EVERYONE;
if (this.modelRegistryAccessLevel > featureAccessLevel.NOT_ENABLED)
this.modelRegistryAccessLevel = featureAccessLevel.EVERYONE;
if (this.snippetsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.snippetsAccessLevel = featureAccessLevel.EVERYONE;
if (this.pagesAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
@ -913,6 +923,19 @@ export default {
name="project[project_feature_attributes][model_experiments_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="model-registry-settings"
:label="$options.i18n.modelRegistryLabel"
:help-text="$options.i18n.modelRegistryHelpText"
:help-path="$options.modelRegistryHelpPath"
>
<project-feature-setting
v-model="modelRegistryAccessLevel"
:label="$options.i18n.modelRegistryLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][model_registry_access_level]"
/>
</project-setting-row>
<project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
ref="pages-settings"

View File

@ -48,3 +48,5 @@ export const CVE_ID_REQUEST_BUTTON_I18N = {
export const modelExperimentsHelpPath = helpPagePath(
'user/project/ml/experiment_tracking/index.md',
);
export const modelRegistryHelpPath = helpPagePath('user/project/ml/model_registry/index.md');

View File

@ -467,6 +467,7 @@ class ProjectsController < Projects::ApplicationController
monitor_access_level
infrastructure_access_level
model_experiments_access_level
model_registry_access_level
]
end

View File

@ -9,20 +9,16 @@ module Types
value issue_type.upcase, value: issue_type, description: "#{issue_type.titleize} issue type"
end
value 'TASK', value: 'task',
description: 'Task issue type.',
alpha: { milestone: '15.2' }
value 'OBJECTIVE', value: 'objective',
description: 'Objective issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.6' }
description: 'Objective issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.6' }
value 'KEY_RESULT', value: 'key_result',
description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.7' }
description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.7' }
value 'EPIC', value: 'epic',
description: 'Epic issue type. ' \
'Available only when feature flag `namespace_level_work_items` is enabled.',
alpha: { milestone: '16.7' }
description: 'Epic issue type. ' \
'Available only when feature flag `namespace_level_work_items` is enabled.',
alpha: { milestone: '16.7' }
end
end

View File

@ -687,7 +687,8 @@ module ProjectsHelper
featureFlagsAccessLevel: feature.feature_flags_access_level,
releasesAccessLevel: feature.releases_access_level,
infrastructureAccessLevel: feature.infrastructure_access_level,
modelExperimentsAccessLevel: feature.model_experiments_access_level
modelExperimentsAccessLevel: feature.model_experiments_access_level,
modelRegistryAccessLevel: feature.model_registry_access_level
}
end

View File

@ -93,7 +93,7 @@ module WorkItems
end
def self.allowed_types_for_issues
base_types.keys.excluding('task', 'objective', 'key_result', 'epic', 'ticket')
base_types.keys.excluding('objective', 'key_result', 'epic', 'ticket')
end
def default?

View File

@ -1,8 +0,0 @@
---
name: ci_editor_assistant_tool
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130162
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423524
milestone: '16.4'
type: development
group: group::environments
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: create_project_subscription_graphql_endpoint
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133308
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429339
milestone: '16.6'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -30452,7 +30452,7 @@ Issue type.
| <a id="issuetypekey_result"></a>`KEY_RESULT` **{warning-solid}** | **Introduced** in 15.7. This feature is an Experiment. It can be changed or removed at any time. Key Result issue type. Available only when feature flag `okrs_mvc` is enabled. |
| <a id="issuetypeobjective"></a>`OBJECTIVE` **{warning-solid}** | **Introduced** in 15.6. This feature is an Experiment. It can be changed or removed at any time. Objective issue type. Available only when feature flag `okrs_mvc` is enabled. |
| <a id="issuetyperequirement"></a>`REQUIREMENT` | Requirement issue type. |
| <a id="issuetypetask"></a>`TASK` **{warning-solid}** | **Introduced** in 15.2. This feature is an Experiment. It can be changed or removed at any time. Task issue type. |
| <a id="issuetypetask"></a>`TASK` | Task issue type. |
| <a id="issuetypetest_case"></a>`TEST_CASE` | Test Case issue type. |
### `IterationSearchableField`

View File

@ -200,6 +200,35 @@ by a reviewer before passing it to a maintainer as described in the
Designers do not require a Product Designer to approve feature changes, unless the changes are community contributions.
1. End-to-end changes include all files in the `qa` directory.
#### CODEOWNERS approval
Some merge requests require mandatory approval by specific groups.
See `.gitlab/CODEOWNERS` for definitions.
Mandatory sections in `.gitlab/CODEOWNERS` should only be limited to cases where
it is necessary due to:
- compliance
- availability
- security
When adding a mandatory section, you should track the impact on the new mandatory section
on merge request rates.
See the [Verify issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411559) for a good example.
All other cases should not use mandatory sections as we favor
[responsbility over ridigity](https://handbook.gitlab.com/handbook/values/#freedom-and-responsibility-over-rigidity).
Additionally, the current structure of the monolith means that merge requests
are likely to touch seemingly un-related parts.
Multiple mandatory approvals means that such merge requests require the author
to seek approvals, which is not efficient.
Efforts to improve this are in:
- <https://gitlab.com/groups/gitlab-org/-/epics/11624>
- <https://gitlab.com/gitlab-org/gitlab/-/issues/377326>
#### Acceptance checklist
This checklist encourages the authors, reviewers, and maintainers of merge requests (MRs) to confirm changes were analyzed for high-impact risks to quality, performance, reliability, security, observability, and maintainability.

View File

@ -336,7 +336,7 @@ cannot change them:
non-modifiable and are always run.
- Explicitly set any [variables](../../ci/yaml/index.md#variables) the job references. This:
- Ensures that project-level pipeline configurations do not set them and alter their
behavior.
behavior. For example, see `before_script` and `after_script` configuration in the [example configuration](#example-configuration).
- Includes any jobs that drive the logic of your job.
- Explicitly set the [container image](../../ci/yaml/index.md#image) to run the job in. This ensures that your script
steps execute in the correct environment.

View File

@ -27,8 +27,10 @@ which you can customize to meet the specific needs of each project.
- Verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/)
is defined so that volumes can be dynamically provisioned for each workspace.
- Install an Ingress controller of your choice (for example, `ingress-nginx`) and make
that controller accessible over a domain. For example, point `*.workspaces.example.dev`
and `workspaces.example.dev` to the load balancer exposed by the Ingress controller.
that controller accessible over a domain.
- In development environments, add an entry to the `/etc/hosts` file or update your DNS records.
- In production environments, point `*.<workspaces.example.dev>` and `<workspaces.example.dev>`
to the load balancer exposed by the Ingress controller.
- [Install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions).
- [Install](../clusters/agent/install/index.md) and [configure](gitlab_agent_configuration.md) the GitLab agent.
- You must have at least the Developer role in the root group.

View File

@ -15,7 +15,7 @@ module Gitlab
end
def execute
return unless project.import_data
return unless import_data_valid?
log_info(import_stage: 'import_pull_request_notes', message: 'starting', iid: object[:iid])
@ -45,6 +45,10 @@ module Gitlab
attr_reader :object, :project, :formatter, :user_finder
def import_data_valid?
project.import_data&.credentials && project.import_data&.data
end
# rubocop: disable CodeReuse/ActiveRecord
def import_merge_event(merge_request, merge_event)
log_info(import_stage: 'import_merge_event', message: 'starting', iid: merge_request.iid)

View File

@ -172,6 +172,11 @@ msgid_plural "%d authors"
msgstr[0] ""
msgstr[1] ""
msgid "%d candidate"
msgid_plural "%d candidates"
msgstr[0] ""
msgstr[1] ""
msgid "%d changed file"
msgid_plural "%d changed files"
msgstr[0] ""
@ -30925,7 +30930,10 @@ msgstr ""
msgid "MlModelRegistry|Experiment"
msgstr ""
msgid "MlModelRegistry|Failed to load model versions"
msgid "MlModelRegistry|Failed to load model candidates with error: %{message}"
msgstr ""
msgid "MlModelRegistry|Failed to load model versions with error: %{message}"
msgstr ""
msgid "MlModelRegistry|ID"
@ -30973,6 +30981,9 @@ msgstr ""
msgid "MlModelRegistry|Status"
msgstr ""
msgid "MlModelRegistry|This model has no candidates"
msgstr ""
msgid "MlModelRegistry|This model has no versions"
msgstr ""
@ -38014,6 +38025,9 @@ msgstr ""
msgid "ProjectSettings|Make sure this pattern does not contradict the %{link_start}Push rules &gt; Branch name%{link_end} setting."
msgstr ""
msgid "ProjectSettings|Manage machine learning models."
msgstr ""
msgid "ProjectSettings|Manage who can see the project in the public access directory."
msgstr ""
@ -38059,6 +38073,9 @@ msgstr ""
msgid "ProjectSettings|Model experiments"
msgstr ""
msgid "ProjectSettings|Model registry"
msgstr ""
msgid "ProjectSettings|Monitor"
msgstr ""

View File

@ -1079,10 +1079,10 @@ RSpec.describe Projects::IssuesController, :request_store, feature_category: :te
end
context 'when trying to create a task' do
it 'defaults to issue type' do
it 'sets the correct issue_type' do
issue = post_new_issue(issue_type: 'task')
expect(issue.work_item_type.base_type).to eq('issue')
expect(issue.work_item_type.base_type).to eq('task')
end
end

View File

@ -1093,6 +1093,7 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
monitor_access_level
infrastructure_access_level
model_experiments_access_level
model_registry_access_level
]
end

View File

@ -1,16 +1,28 @@
import { GlBadge, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { ShowMlModel } from '~/ml/model_registry/apps';
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
import CandidateList from '~/ml/model_registry/components/candidate_list.vue';
import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
import createMockApollo from 'helpers/mock_apollo_helper';
import { MODEL, makeModel } from '../mock_data';
const apolloProvider = createMockApollo([]);
let wrapper;
Vue.use(VueApollo);
const createWrapper = (model = MODEL) => {
wrapper = shallowMount(ShowMlModel, { propsData: { model }, stubs: { GlTab } });
wrapper = shallowMount(ShowMlModel, {
apolloProvider,
propsData: { model },
stubs: { GlTab },
});
};
const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0);
@ -19,6 +31,7 @@ const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge);
const findModelVersionList = () => findVersionsTab().findComponent(ModelVersionList);
const findModelVersionDetail = () => findDetailTab().findComponent(ModelVersionDetail);
const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2);
const findCandidateList = () => findCandidateTab().findComponent(CandidateList);
const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge);
const findTitleArea = () => wrapper.findComponent(TitleArea);
const findVersionCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
@ -90,5 +103,9 @@ describe('ShowMlModel', () => {
it('shows the number of candidates in the tab', () => {
expect(findCandidatesCountBadge().text()).toBe(MODEL.candidateCount.toString());
});
it('shows a list of candidates', () => {
expect(findCandidateList().props('modelId')).toBe(MODEL.id);
});
});
});

View File

@ -0,0 +1,39 @@
import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
import { graphqlCandidates } from '../graphql_mock_data';
const CANDIDATE = graphqlCandidates[0];
let wrapper;
const createWrapper = (candidate = CANDIDATE) => {
wrapper = shallowMount(CandidateListRow, {
propsData: { candidate },
stubs: {
GlSprintf,
GlTruncate,
},
});
};
const findListItem = () => wrapper.findComponent(ListItem);
const findLink = () => findListItem().findComponent(GlLink);
const findTruncated = () => findLink().findComponent(GlTruncate);
const findTooltip = () => findListItem().findComponent(TimeAgoTooltip);
describe('ml/model_registry/components/candidate_list_row.vue', () => {
beforeEach(() => {
createWrapper();
});
it('Has a link to the candidate', () => {
expect(findTruncated().props('text')).toBe(CANDIDATE.name);
expect(findLink().attributes('href')).toBe(CANDIDATE._links.showPath);
});
it('Shows created at', () => {
expect(findTooltip().props('time')).toBe(CANDIDATE.createdAt);
});
});

View File

@ -0,0 +1,182 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CandidateList from '~/ml/model_registry/components/candidate_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
import getModelCandidatesQuery from '~/ml/model_registry/graphql/queries/get_model_candidates.query.graphql';
import { GRAPHQL_PAGE_SIZE } from '~/ml/model_registry/constants';
import {
emptyCandidateQuery,
modelCandidatesQuery,
graphqlCandidates,
graphqlPageInfo,
} from '../graphql_mock_data';
Vue.use(VueApollo);
describe('ml/model_registry/components/candidate_list.vue', () => {
let wrapper;
let apolloProvider;
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoader = () => wrapper.findComponent(PackagesListLoader);
const findRegistryList = () => wrapper.findComponent(RegistryList);
const findListRow = () => wrapper.findComponent(CandidateListRow);
const findAllRows = () => wrapper.findAllComponents(CandidateListRow);
const mountComponent = ({
props = {},
resolver = jest.fn().mockResolvedValue(modelCandidatesQuery()),
} = {}) => {
const requestHandlers = [[getModelCandidatesQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(CandidateList, {
apolloProvider,
propsData: {
modelId: 2,
...props,
},
stubs: {
RegistryList,
},
});
};
beforeEach(() => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
describe('when list is loaded and has no data', () => {
const resolver = jest.fn().mockResolvedValue(emptyCandidateQuery);
beforeEach(async () => {
mountComponent({ resolver });
await waitForPromises();
});
it('displays empty slot message', () => {
expect(wrapper.text()).toContain('This model has no candidates');
});
it('does not display loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('does not display rows', () => {
expect(findListRow().exists()).toBe(false);
});
it('does not display registry list', () => {
expect(findRegistryList().exists()).toBe(false);
});
it('does not display alert', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('if load fails, alert', () => {
beforeEach(async () => {
const error = new Error('Failure!');
mountComponent({ resolver: jest.fn().mockRejectedValue(error) });
await waitForPromises();
});
it('is displayed', () => {
expect(findAlert().exists()).toBe(true);
});
it('shows error message', () => {
expect(findAlert().text()).toContain('Failed to load model candidates with error: Failure!');
});
it('is not dismissible', () => {
expect(findAlert().props('dismissible')).toBe(false);
});
it('is of variant danger', () => {
expect(findAlert().attributes('variant')).toBe('danger');
});
it('error is logged in sentry', () => {
expect(Sentry.captureException).toHaveBeenCalled();
});
});
describe('when list is loaded with data', () => {
beforeEach(async () => {
mountComponent();
await waitForPromises();
});
it('displays package registry list', () => {
expect(findRegistryList().exists()).toEqual(true);
});
it('binds the right props', () => {
expect(findRegistryList().props()).toMatchObject({
items: graphqlCandidates,
pagination: {},
isLoading: false,
hiddenDelete: true,
});
});
it('displays candidate rows', () => {
expect(findAllRows().exists()).toEqual(true);
expect(findAllRows()).toHaveLength(graphqlCandidates.length);
});
it('binds the correct props', () => {
expect(findAllRows().at(0).props()).toMatchObject({
candidate: expect.objectContaining(graphqlCandidates[0]),
});
expect(findAllRows().at(1).props()).toMatchObject({
candidate: expect.objectContaining(graphqlCandidates[1]),
});
});
it('does not display loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('does not display empty message', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('when user interacts with pagination', () => {
const resolver = jest.fn().mockResolvedValue(modelCandidatesQuery());
beforeEach(async () => {
mountComponent({ resolver });
await waitForPromises();
});
it('when list emits next-page fetches the next set of records', async () => {
findRegistryList().vm.$emit('next-page');
await waitForPromises();
expect(resolver).toHaveBeenLastCalledWith(
expect.objectContaining({ after: graphqlPageInfo.endCursor, first: GRAPHQL_PAGE_SIZE }),
);
});
it('when list emits prev-page fetches the prev set of records', async () => {
findRegistryList().vm.$emit('prev-page');
await waitForPromises();
expect(resolver).toHaveBeenLastCalledWith(
expect.objectContaining({ before: graphqlPageInfo.startCursor, last: GRAPHQL_PAGE_SIZE }),
);
});
});
});

View File

@ -17,7 +17,7 @@ import {
modelVersionsQuery,
graphqlModelVersions,
graphqlPageInfo,
} from '../mock_data';
} from '../graphql_mock_data';
Vue.use(VueApollo);
@ -85,7 +85,8 @@ describe('ModelVersionList', () => {
describe('if load fails, alert', () => {
beforeEach(async () => {
mountComponent({ resolver: jest.fn().mockRejectedValue() });
const error = new Error('Failure!');
mountComponent({ resolver: jest.fn().mockRejectedValue(error) });
await waitForPromises();
});
@ -95,7 +96,7 @@ describe('ModelVersionList', () => {
});
it('shows error message', () => {
expect(findAlert().text()).toMatchInterpolatedText('Failed to load model versions');
expect(findAlert().text()).toContain('Failed to load model versions with error: Failure!');
});
it('is not dismissible', () => {

View File

@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import ModelVersionRow from '~/ml/model_registry/components/model_version_row.vue';
import { graphqlModelVersions } from '../mock_data';
import { graphqlModelVersions } from '../graphql_mock_data';
let wrapper;
const createWrapper = (modelVersion = graphqlModelVersions[0]) => {

View File

@ -0,0 +1,116 @@
import { defaultPageInfo } from './mock_data';
export const graphqlPageInfo = {
...defaultPageInfo,
__typename: 'PageInfo',
};
export const graphqlModelVersions = [
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Ml::ModelVersion/243',
version: '1.0.1',
_links: {
showPath: '/path/to/modelversion/243',
},
__typename: 'MlModelVersion',
},
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Ml::ModelVersion/244',
version: '1.0.2',
_links: {
showPath: '/path/to/modelversion/244',
},
__typename: 'MlModelVersion',
},
];
export const modelVersionsQuery = (versions = graphqlModelVersions) => ({
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/2',
versions: {
count: versions.length,
nodes: versions,
pageInfo: graphqlPageInfo,
__typename: 'MlModelConnection',
},
__typename: 'MlModelType',
},
},
});
export const graphqlCandidates = [
{
id: 'gid://gitlab/Ml::Candidate/1',
name: 'narwhal-aardvark-heron-6953',
createdAt: '2023-12-06T12:41:48Z',
_links: {
showPath: '/path/to/candidate/1',
},
},
{
id: 'gid://gitlab/Ml::Candidate/2',
name: 'anteater-chimpanzee-snake-1254',
createdAt: '2023-12-06T12:41:48Z',
_links: {
showPath: '/path/to/candidate/2',
},
},
];
export const modelCandidatesQuery = (candidates = graphqlCandidates) => ({
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/2',
candidates: {
count: candidates.length,
nodes: candidates,
pageInfo: graphqlPageInfo,
__typename: 'MlCandidateConnection',
},
__typename: 'MlModelType',
},
},
});
export const emptyModelVersionsQuery = {
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/2',
versions: {
count: 0,
nodes: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
endCursor: 'endCursor',
startCursor: 'startCursor',
},
__typename: 'MlModelConnection',
},
__typename: 'MlModelType',
},
},
};
export const emptyCandidateQuery = {
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/2',
candidates: {
count: 0,
nodes: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
endCursor: 'endCursor',
startCursor: 'startCursor',
},
__typename: 'MlCandidateConnection',
},
__typename: 'MlModelType',
},
},
};

View File

@ -102,64 +102,3 @@ export const defaultPageInfo = Object.freeze({
hasNextPage: true,
hasPreviousPage: true,
});
export const graphqlPageInfo = {
...defaultPageInfo,
__typename: 'PageInfo',
};
export const graphqlModelVersions = [
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Ml::ModelVersion/243',
version: '1.0.1',
_links: {
showPath: '/path/to/modelversion/243',
},
__typename: 'MlModelVersion',
},
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Ml::ModelVersion/244',
version: '1.0.2',
_links: {
showPath: '/path/to/modelversion/244',
},
__typename: 'MlModelVersion',
},
];
export const modelVersionsQuery = (versions = graphqlModelVersions) => ({
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/2',
versions: {
count: versions.length,
nodes: versions,
pageInfo: graphqlPageInfo,
__typename: 'MlModelConnection',
},
__typename: 'MlModelType',
},
},
});
export const emptyModelVersionsQuery = {
data: {
mlModel: {
id: 'gid://gitlab/Ml::Model/2',
versions: {
count: 0,
nodes: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
endCursor: 'endCursor',
startCursor: 'startCursor',
},
__typename: 'MlModelConnection',
},
__typename: 'MlModelType',
},
},
};

View File

@ -137,6 +137,7 @@ describe('Settings Panel', () => {
const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
const findModelExperimentsSettings = () =>
wrapper.findComponent({ ref: 'model-experiments-settings' });
const findModelRegistrySettings = () => wrapper.findComponent({ ref: 'model-registry-settings' });
describe('Project Visibility', () => {
it('should set the project visibility help path', () => {
@ -758,4 +759,11 @@ describe('Settings Panel', () => {
expect(findModelExperimentsSettings().exists()).toBe(true);
});
});
describe('Model registry', () => {
it('shows model registry toggle', () => {
wrapper = mountComponent({});
expect(findModelRegistrySettings().exists()).toBe(true);
});
});
});

View File

@ -892,7 +892,8 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do
featureFlagsAccessLevel: project.project_feature.feature_flags_access_level,
releasesAccessLevel: project.project_feature.releases_access_level,
infrastructureAccessLevel: project.project_feature.infrastructure_access_level,
modelExperimentsAccessLevel: project.project_feature.model_experiments_access_level
modelExperimentsAccessLevel: project.project_feature.model_experiments_access_level,
modelRegistryAccessLevel: project.project_feature.model_registry_access_level
)
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporter, feature_category: :importers do
include AfterNextHelpers
let_it_be(:project) do
let_it_be_with_reload(:project) do
create(:project, :repository, :import_started,
import_data_attributes: {
data: { 'project_key' => 'key', 'repo_slug' => 'slug' },
@ -82,19 +82,6 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporte
subject(:importer) { described_class.new(project.reload, pull_request.to_hash) }
describe '#execute' do
context 'when the project has been marked as failed' do
before do
project.import_state.mark_as_failed('error')
end
it 'does not log and does not import notes' do
expect(Gitlab::BitbucketServerImport::Logger)
.not_to receive(:info).with(include(import_stage: 'import_pull_request_notes', message: 'starting'))
expect { importer.execute }.not_to change { Note.count }
end
end
context 'when a matching merge request is not found' do
it 'does nothing' do
expect { importer.execute }.not_to change { Note.count }
@ -312,5 +299,40 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestNotesImporte
end
end
end
shared_examples 'import is skipped' do
it 'does not log and does not import notes' do
expect(Gitlab::BitbucketServerImport::Logger)
.not_to receive(:info).with(include(import_stage: 'import_pull_request_notes', message: 'starting'))
expect { importer.execute }.not_to change { Note.count }
end
end
context 'when the project has been marked as failed' do
before do
project.import_state.mark_as_failed('error')
end
include_examples 'import is skipped'
end
context 'when the import data does not have credentials' do
before do
project.import_data.credentials = nil
project.import_data.save!
end
include_examples 'import is skipped'
end
context 'when the import data does not have data' do
before do
project.import_data.data = nil
project.import_data.save!
end
include_examples 'import is skipped'
end
end
end

View File

@ -20,7 +20,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) }
let_it_be(:task) { create(:issue, :task, author: user, project: project) }
let_it_be(:objective) { create(:issue, :objective, author: user, project: project) }
let_it_be(:closed_issue) do
create :closed_issue,