Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-27 18:19:32 +00:00
parent 05be289de1
commit 9a66377d7f
87 changed files with 1342 additions and 390 deletions

View File

@ -306,6 +306,59 @@ rspec system pg14 praefect:
- .rspec-system-parallel
- .rails:rules:praefect-with-db
# Test jobs that run without Gitaly's transactions enabled. These will be removed once
# transactions are always in use in Gitaly.
rspec migration pg14 no_gitaly_transactions:
extends:
- rspec migration pg14
- .gitaly-without-transactions
rspec background_migration pg14 no_gitaly_transactions:
extends:
- rspec background_migration pg14
- .gitaly-without-transactions
rspec unit pg14 no_gitaly_transactions:
extends:
- rspec unit pg14
- .gitaly-without-transactions
rspec integration pg14 no_gitaly_transactions:
extends:
- rspec integration pg14
- .gitaly-without-transactions
rspec system pg14 no_gitaly_transactions:
extends:
- rspec system pg14
- .gitaly-without-transactions
rspec-ee migration pg14 no_gitaly_transactions:
extends:
- rspec-ee migration pg14
- .gitaly-without-transactions
rspec-ee background_migration pg14 no_gitaly_transactions:
extends:
- rspec-ee background_migration pg14
- .gitaly-without-transactions
rspec-ee unit pg14 no_gitaly_transactions:
extends:
- rspec-ee unit pg14
- .gitaly-without-transactions
rspec-ee integration pg14 no_gitaly_transactions:
extends:
- rspec-ee integration pg14
- .gitaly-without-transactions
rspec-ee system pg14 no_gitaly_transactions:
extends:
- rspec-ee system pg14
- .gitaly-without-transactions
# Dedicated job to test DB library code against PG13.
# Note that these are already tested against PG13 in the `rspec unit pg13` / `rspec-ee unit pg13` jobs.
rspec db-library-code pg13:

View File

@ -56,6 +56,16 @@ include:
variables:
GITALY_PRAEFECT_WITH_DB: '1'
.gitaly-with-transactions:
variables:
GITALY_TRANSACTIONS_ENABLED: "true"
.gitaly-without-transactions:
extends:
- .rails:rules:gitaly-without-transactions
variables:
GITALY_TRANSACTIONS_ENABLED: "false"
.rspec-base-needs:
needs:
- job: "clone-gitlab-repo"
@ -68,6 +78,7 @@ include:
- .rails-job-base
- .base-artifacts
- .repo-from-artifacts
- .gitaly-with-transactions
stage: test
variables:
RUBY_GC_MALLOC_LIMIT: 67108864

View File

@ -2039,6 +2039,11 @@
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-praefect-with-db/'
allow_failure: true
.rails:rules:gitaly-without-transactions:
rules:
- <<: *if-schedule-maintenance
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-without-gitaly-transactions/'
.rails:rules:ee-and-foss-migration:
rules:
- <<: *if-fork-merge-request

View File

@ -73,19 +73,6 @@ Layout/ArgumentAlignment:
- 'ee/spec/requests/api/deployments_spec.rb'
- 'ee/spec/requests/api/dora/metrics_spec.rb'
- 'ee/spec/requests/api/epics_spec.rb'
- 'ee/spec/requests/api/graphql/ci/runner_spec.rb'
- 'ee/spec/requests/api/graphql/group/dast_profile_schedule_spec.rb'
- 'ee/spec/requests/api/graphql/group/epic/epic_children_spec.rb'
- 'ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb'
- 'ee/spec/requests/api/graphql/group/epics_spec.rb'
- 'ee/spec/requests/api/graphql/merge_request_reviewer_spec.rb'
- 'ee/spec/requests/api/graphql/merge_requests/approval_state_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/boards/lists/update_limit_metrics_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/boards/update_epic_user_preferences_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/destroy_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/update_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/epic_tree/reorder_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/issues/set_weight_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/iterations/cadences/create_spec.rb'
@ -307,19 +294,6 @@ Layout/ArgumentAlignment:
- 'lib/gitlab/sidekiq_config/worker.rb'
- 'lib/gitlab/spamcheck/client.rb'
- 'lib/gitlab/usage/metrics/instrumentations/database_metric.rb'
- 'lib/gitlab/usage_data.rb'
- 'lib/gitlab/usage_data_counters/ci_template_unique_counter.rb'
- 'lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb'
- 'lib/peek/views/detailed_view.rb'
- 'lib/peek/views/redis_detailed.rb'
- 'lib/tasks/gems.rake'
- 'lib/tasks/gitlab/feature_categories.rake'
- 'lib/tasks/gitlab/seed.rake'
- 'lib/tasks/gitlab/seed/runner_fleet.rake'
- 'lib/tasks/gitlab/shell.rake'
- 'lib/tasks/gitlab/uploads/sanitize.rake'
- 'rubocop/cop/gitlab/finder_with_find_by.rb'
- 'rubocop/cop/qa/ambiguous_page_object_name.rb'
- 'rubocop/cop/rspec/modify_sidekiq_middleware.rb'
- 'scripts/packages/automated_cleanup.rb'
- 'scripts/rubocop-parse'

View File

@ -13,7 +13,6 @@ Lint/SymbolConversion:
- 'app/graphql/resolvers/environments/last_deployment_resolver.rb'
- 'app/graphql/resolvers/user_discussions_count_resolver.rb'
- 'app/graphql/resolvers/user_notes_count_resolver.rb'
- 'app/helpers/breadcrumbs_helper.rb'
- 'app/helpers/notifications_helper.rb'
- 'app/helpers/projects_helper.rb'
- 'app/models/application_record.rb'
@ -28,7 +27,6 @@ Lint/SymbolConversion:
- 'app/models/work_items/widgets/assignees.rb'
- 'app/models/work_items/widgets/base.rb'
- 'app/presenters/snippet_presenter.rb'
- 'app/services/cloud_seed/google_cloud/create_cloudsql_instance_service.rb'
- 'app/services/concerns/deploy_token_methods.rb'
- 'app/services/concerns/rate_limited_service.rb'
- 'app/services/container_expiration_policies/cleanup_service.rb'
@ -37,30 +35,22 @@ Lint/SymbolConversion:
- 'app/services/notification_recipients/builder/default.rb'
- 'app/services/notification_service.rb'
- 'app/workers/project_export_worker.rb'
- 'ee/app/components/billing/plan_component.rb'
- 'ee/app/controllers/projects/security/scanned_resources_controller.rb'
- 'ee/app/graphql/ee/resolvers/projects/branch_rules_resolver.rb'
- 'ee/app/graphql/types/ci/jobs_duration_statistics_type.rb'
- 'ee/app/models/concerns/ee/protected_branch.rb'
- 'ee/app/models/geo_node_status.rb'
- 'ee/app/policies/ee/group_policy.rb'
- 'ee/app/policies/ee/project_policy.rb'
- 'ee/app/serializers/integrations/zentao_serializers/issue_entity.rb'
- 'ee/app/serializers/report_list_entity.rb'
- 'ee/app/services/security/security_orchestration_policies/ci_action/base.rb'
- 'ee/app/services/vulnerabilities/manually_create_service.rb'
- 'ee/app/workers/security/store_scans_worker.rb'
- 'ee/db/fixtures/development/35_merge_request_predictions.rb'
- 'ee/lib/api/concerns/dependency_proxy/packages_helpers.rb'
- 'ee/lib/ee/api/helpers.rb'
- 'ee/lib/elastic/latest/note_class_proxy.rb'
- 'ee/lib/gitlab/applied_ml/suggested_reviewers/client.rb'
- 'ee/lib/gitlab/elastic/search_results.rb'
- 'ee/lib/gitlab/geo/replicator.rb'
- 'ee/lib/gitlab/graphql/aggregations/epics/epic_node.rb'
- 'ee/lib/search/zoekt/search_results.rb'
- 'ee/spec/controllers/admin/audit_logs_controller_spec.rb'
- 'ee/spec/controllers/groups/audit_events_controller_spec.rb'
- 'ee/spec/controllers/projects/audit_events_controller_spec.rb'
- 'ee/spec/factories/ci/builds.rb'
- 'ee/spec/factories/ci/pipelines.rb'
@ -141,21 +131,6 @@ Lint/SymbolConversion:
- 'spec/lib/api/entities/nuget/search_result_spec.rb'
- 'spec/lib/api/helpers/rate_limiter_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/project_feature_pipeline_spec.rb'
- 'spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb'
- 'spec/lib/constraints/group_url_constrainer_spec.rb'
- 'spec/lib/constraints/project_url_constrainer_spec.rb'
- 'spec/lib/constraints/user_url_constrainer_spec.rb'
- 'spec/lib/container_registry/gitlab_api_client_spec.rb'
- 'spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb'
- 'spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/job_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/jobs_spec.rb'
- 'spec/lib/gitlab/ci/config/entry/processable_spec.rb'
- 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
- 'spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
- 'spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
- 'spec/lib/gitlab/ci/reports/codequality_reports_spec.rb'
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
- 'spec/lib/google_api/cloud_platform/client_spec.rb'
- 'spec/lib/service_ping/devops_report_spec.rb'
- 'spec/models/appearance_spec.rb'

View File

@ -176,6 +176,17 @@ const Api = {
});
},
groupSubgroups(id, options) {
const url = Api.buildUrl(this.subgroupsPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: {
per_page: DEFAULT_PER_PAGE,
...options,
},
});
},
inviteGroupMembers(id, data) {
const url = Api.buildUrl(this.groupInvitationsPath).replace(':id', encodeURIComponent(id));

View File

@ -922,7 +922,7 @@ export default {
:key="activeIssuable.iid"
:work-item-iid="activeIssuable.iid"
is-drawer
class="gl-pt-0!"
class="gl-pt-0! work-item-drawer"
@work-item-updated="updateIssuablesCache"
@work-item-emoji-updated="updateIssuableEmojis"
@addChild="refetchIssuables"

View File

@ -1,5 +1,5 @@
<script>
import { GlExperimentBadge, GlButton } from '@gitlab/ui';
import { GlExperimentBadge } from '@gitlab/ui';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
@ -8,6 +8,7 @@ import EmptyState from '../components/empty_state.vue';
import * as i18n from '../translations';
import { BASE_SORT_FIELDS, MODEL_ENTITIES } from '../constants';
import ModelRow from '../components/model_row.vue';
import ModelCreate from '../components/model_create.vue';
import ActionsDropdown from '../components/actions_dropdown.vue';
import getModelsQuery from '../graphql/queries/get_models.query.graphql';
import { makeLoadModelErrorMessage } from '../translations';
@ -17,10 +18,10 @@ export default {
name: 'IndexMlModels',
components: {
ModelRow,
ModelCreate,
MetadataItem,
TitleArea,
GlExperimentBadge,
GlButton,
EmptyState,
ActionsDropdown,
SearchableList,
@ -28,6 +29,7 @@ export default {
provide() {
return {
mlflowTrackingUrl: this.mlflowTrackingUrl,
projectPath: this.projectPath,
};
},
props: {
@ -35,10 +37,6 @@ export default {
type: String,
required: true,
},
createModelPath: {
type: String,
required: true,
},
canWriteModelRegistry: {
type: Boolean,
required: false,
@ -129,12 +127,7 @@ export default {
<metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(count)" />
</template>
<template #right-actions>
<gl-button
v-if="canWriteModelRegistry"
:href="createModelPath"
data-testid="create-model-button"
>{{ $options.i18n.CREATE_MODEL_LABEL }}</gl-button
>
<model-create v-if="canWriteModelRegistry" />
<actions-dropdown />
</template>

View File

@ -0,0 +1,234 @@
<script>
import {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlModal,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { uploadModel } from '../services/upload_model';
import createModelVersionMutation from '../graphql/mutations/create_model_version.mutation.graphql';
import createModelMutation from '../graphql/mutations/create_model.mutation.graphql';
export default {
name: 'ModelCreate',
components: {
GlAlert,
GlButton,
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlFormTextarea,
ImportArtifactZone: () => import('./import_artifact_zone.vue'),
},
inject: ['projectPath'],
data() {
return {
name: null,
version: null,
description: null,
versionDescription: null,
modalVisible: false,
errorMessage: null,
selectedFile: null,
modelData: null,
};
},
computed: {
showImportArtifactZone() {
return this.version && this.name;
},
},
methods: {
async createModel() {
const { data } = await this.$apollo.mutate({
mutation: createModelMutation,
variables: {
projectPath: this.projectPath,
name: this.name,
description: this.description,
},
});
return data;
},
async createModelVersion(modelGid) {
const { data } = await this.$apollo.mutate({
mutation: createModelVersionMutation,
variables: {
projectPath: this.projectPath,
modelId: modelGid,
version: this.version,
description: this.versionDescription,
},
});
return data;
},
async create($event) {
$event.preventDefault();
this.errorMessage = '';
try {
// attempt creating a model
if (!this.modelData) {
this.modelData = await this.createModel();
}
const modelErrors = this.modelData?.mlModelCreate?.errors || [];
if (modelErrors.length) {
this.errorMessage = modelErrors.join(', ');
} else if (this.version) {
// model creation is successful, attempt creating a version
const versionData = await this.createModelVersion(this.modelData.mlModelCreate.model.id);
const versionErrors = versionData?.mlModelVersionCreate?.errors || [];
if (versionErrors.length) {
this.errorMessage = versionErrors.join(', ');
} else {
// attempt importing model artifacts
const { importPath } = versionData.mlModelVersionCreate.modelVersion._links;
await uploadModel({ importPath, file: this.selectedFile });
const { showPath } = versionData.mlModelVersionCreate.modelVersion._links;
visitUrl(showPath);
}
} else {
const { showPath } = this.modelData.mlModelCreate.model._links;
visitUrl(showPath);
}
} catch (error) {
Sentry.captureException(error);
this.errorMessage = s__(
'MlModelRegistry|Error creating model, version and uploading artifacts. Please try again.',
);
}
},
showCreateModal() {
this.modalVisible = true;
},
cancelModal() {
this.modalVisible = false;
},
hideAlert() {
this.errorMessage = null;
},
},
i18n: {},
modal: {
id: 'ml-experiments-delete-modal',
actionPrimary: {
text: __('Create'),
attributes: { variant: 'confirm' },
},
actionCancel: {
text: __('Cancel'),
},
nameDescription: s__(
'MlModelRegistry|Model name must not contain spaces or upper case letter.',
),
namePlaceholder: s__('MlModelRegistry|For example my-model'),
versionDescription: s__('MlModelRegistry|Leave empty to skip version creation.'),
versionPlaceholder: s__('MlModelRegistry|For example 1.0.0'),
descriptionPlaceholder: s__('MlModelRegistry|Enter some model description'),
versionDescriptionTitle: s__('MlModelRegistry|Version Description'),
versionDescriptionPlaceholder: s__(
'MlModelRegistry|Initial version name. Must be a semantic version.',
),
buttonTitle: s__('MlModelRegistry|Create model'),
title: s__('MlModelRegistry|Create model, version & import artifacts'),
modelName: s__('MlModelRegistry|Model name'),
modelDescription: __('Description'),
version: __('Version'),
import: __('Import'),
modelSuccessButVersionArtifactFailAlert: {
id: 'ml-model-success-version-artifact-failed',
message: s__(
'MlModelRegistry|Model has been created but version or artifacts could not be uploaded. Try creating model version.',
),
variant: 'warning',
},
},
};
</script>
<template>
<div>
<gl-button @click="showCreateModal">{{ $options.modal.buttonTitle }}</gl-button>
<gl-modal
v-model="modalVisible"
modal-id="create-model-modal"
:title="$options.modal.title"
:action-primary="$options.modal.actionPrimary"
:action-cancel="$options.modal.actionCancel"
size="sm"
@primary="create"
@cancel="cancelModal"
>
<gl-form>
<gl-form-group
:label="$options.modal.modelName"
label-for="nameId"
:description="$options.modal.nameDescription"
>
<gl-form-input
id="nameId"
v-model="name"
data-testid="nameId"
type="text"
:placeholder="$options.modal.namePlaceholder"
/>
</gl-form-group>
<gl-form-group :label="$options.modal.modelDescription" label-for="descriptionId">
<gl-form-textarea
id="descriptionId"
v-model="description"
data-testid="descriptionId"
:placeholder="$options.modal.descriptionPlaceholder"
/>
</gl-form-group>
<gl-form-group
:label="$options.modal.version"
label-for="versionId"
:description="$options.modal.versionDescription"
>
<gl-form-input
id="versionId"
v-model="version"
data-testid="versionId"
type="text"
:placeholder="$options.modal.versionPlaceholder"
/>
</gl-form-group>
<gl-form-group
:label="$options.modal.versionDescriptionTitle"
label-for="versionDescriptionId"
>
<gl-form-textarea
id="versionDescriptionId"
v-model="versionDescription"
data-testid="versionDescriptionId"
:placeholder="$options.modal.versionDescriptionPlaceholder"
/>
</gl-form-group>
<gl-form-group
v-if="showImportArtifactZone"
:label="$options.modal.Import"
label-for="versionImportArtifactZone"
>
<import-artifact-zone
id="versionImportArtifactZone"
v-model="selectedFile"
:submit-on-select="false"
/>
</gl-form-group>
</gl-form>
<gl-alert v-if="errorMessage" variant="danger" @dismiss="hideAlert">{{
errorMessage
}}</gl-alert>
</gl-modal>
</div>
</template>

View File

@ -10,6 +10,7 @@ import {
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { sprintf, n__, s__ } from '~/locale';
import {
getParameterByName,
@ -92,13 +93,13 @@ export default {
projectPath: this.projectPath,
};
},
update({ project: { branchRules } }) {
update({ project: { branchRules, group } }) {
const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch);
this.branchRule = branchRule;
this.branchProtection = branchRule?.branchProtection;
this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
this.matchingBranchesCount = branchRule?.matchingBranchesCount;
this.groupId = getIdFromGraphQLId(group?.id) || '';
if (!this.showApprovers) return;
// The approval rules app uses a separate endpoint to fetch the list of approval rules.
// In future, we will update the GraphQL request to include the approval rules data.
@ -118,6 +119,7 @@ export default {
branchProtection: {},
statusChecks: [],
branchRule: {},
groupId: null,
matchingBranchesCount: null,
isAllowedToMergeDrawerOpen: false,
isRuleUpdating: false,
@ -355,6 +357,7 @@ export default {
:users="mergeAccessLevels.users"
:groups="mergeAccessLevels.groups"
:is-loading="isRuleUpdating"
:group-id="groupId"
:title="s__('BranchRules|Edit allowed to merge')"
@editRule="
editBranchRule({

View File

@ -6,7 +6,7 @@ import ProtectionRow from './protection_row.vue';
export const i18n = {
rolesTitle: s__('BranchRules|Roles'),
usersTitle: s__('BranchRules|Users'),
usersAndGroupsTitle: s__('BranchRules|Users & groups'),
groupsTitle: s__('BranchRules|Groups'),
};
@ -117,20 +117,13 @@ export default {
<!-- Roles -->
<protection-row v-if="roles.length" :title="$options.i18n.rolesTitle" :access-levels="roles" />
<!-- Users -->
<!-- Users and Groups -->
<protection-row
v-if="users.length"
v-if="hasUsers || hasGroups"
:show-divider="hasRoles"
:title="$options.i18n.usersTitle"
:users="users"
/>
<!-- Groups -->
<protection-row
v-if="groups.length"
:show-divider="showDivider"
:title="$options.i18n.groupsTitle"
:access-levels="groups"
:groups="groups"
:title="$options.i18n.usersAndGroupsTitle"
/>
<!-- Status checks -->

View File

@ -36,6 +36,11 @@ export default {
required: false,
default: () => [],
},
groups: {
type: Array,
required: false,
default: () => [],
},
statusCheckUrl: {
type: String,
required: false,
@ -50,6 +55,9 @@ export default {
this.users.length - this.$options.MAX_VISIBLE_AVATARS,
);
},
usersAndGroups() {
return [...this.users, ...this.groups];
},
},
};
</script>
@ -65,9 +73,9 @@ export default {
<div v-if="statusCheckUrl" class="gl-flex-grow-1">{{ statusCheckUrl }}</div>
<gl-avatars-inline
v-if="users.length"
v-if="usersAndGroups.length"
class="!gl-w-1/4"
:avatars="users"
:avatars="usersAndGroups"
:collapsed="true"
:max-visible="$options.MAX_VISIBLE_AVATARS"
:avatar-size="$options.AVATAR_SIZE"
@ -77,7 +85,7 @@ export default {
>
<template #avatar="{ avatar }">
<gl-avatar-link
:key="avatar.username"
:key="avatar.name"
v-gl-tooltip
target="_blank"
:href="avatar.webUrl"
@ -86,6 +94,7 @@ export default {
<gl-avatar
:src="avatar.avatarUrl"
:label="avatar.name"
:entity-name="avatar.name"
:alt="avatar.name"
:size="$options.AVATAR_SIZE"
/>

View File

@ -22,11 +22,13 @@ export default {
},
users: {
type: Array,
required: true,
required: false,
default: () => [],
},
groups: {
type: Array,
required: true,
required: false,
default: () => [],
},
roles: {
type: Array,
@ -36,6 +38,11 @@ export default {
type: String,
required: true,
},
groupId: {
type: Number,
required: false,
default: null,
},
isLoading: {
type: Boolean,
required: true,
@ -43,8 +50,8 @@ export default {
},
data() {
return {
updatedGroups: [],
updatedUsers: [],
updatedGroups: this.groups,
updatedUsers: this.users,
isRuleUpdated: false,
};
},
@ -64,6 +71,7 @@ export default {
getRuleEditData() {
return [
...this.formatItemsData(this.updatedUsers, 'userId', 'User'), // eslint-disable-line @gitlab/require-i18n-strings
...this.formatItemsData(this.updatedGroups, 'groupId', 'Group'), // eslint-disable-line @gitlab/require-i18n-strings
];
},
formatItemsIds(items) {
@ -109,8 +117,17 @@ export default {
:items="formatItemsIds(users)"
is-project-only-namespace
:users-options="$options.projectUsersOptions"
data-testid="users-selector"
@change="handleRuleDataUpdate('updatedUsers', $event)"
/>
<items-selector
type="groups"
:items="formatItemsIds(groups)"
:group-id="groupId"
data-testid="groups-selector"
is-project-only-namespace
@change="handleRuleDataUpdate('updatedGroups', $event)"
/>
</gl-form-group>
</template>
</gl-drawer>

View File

@ -1,6 +1,9 @@
query getBranchRulesDetails($projectPath: ID!) {
project(fullPath: $projectPath) {
id
group {
id
}
branchRules {
nodes {
id

View File

@ -28,7 +28,7 @@ export const getAccessLevels = (accessLevels = {}) => {
const src = node.user.avatarUrl;
accessLevelTypes.users.push({ src, ...node.user });
} else if (node.group) {
accessLevelTypes.groups.push(node);
accessLevelTypes.groups.push(node.group);
} else {
accessLevelTypes.roles.push({ accessLevelDescription: node.accessLevelDescription });
}

View File

@ -25,7 +25,7 @@ export default {
return sprintf(__('Delete %{name}'), { name: this.name });
},
fullName() {
return this.data.fullName;
return this.data.fullName || this.data.name;
},
name() {
return this.data.name;

View File

@ -40,6 +40,16 @@ export default {
required: false,
default: null,
},
groupPath: {
type: String,
required: false,
default: null,
},
groupId: {
type: Number,
required: false,
default: null,
},
autofocus: {
type: Boolean,
required: false,
@ -128,6 +138,10 @@ export default {
},
async fetchGroupsBySearchTerm(search) {
let groups = [];
if (parseBoolean(this.groupId)) {
groups = await this.fetchSubgroupsBySearchTerm(search);
return groups;
}
if (parseBoolean(this.isProjectNamespace)) {
groups = await this.fetchProjectGroups(search);
} else {
@ -164,6 +178,17 @@ export default {
})),
);
},
async fetchSubgroupsBySearchTerm(search) {
let groups = [];
const subgroups = await Api.groupSubgroups(this.groupId, search);
groups = subgroups?.data || [];
return groups?.map((group) => ({
text: group.fullName,
value: group.name,
type: 'group',
...group,
}));
},
fetchDeployKeysBySearchTerm() {
// TODO - implement API request (follow-up)
// https://gitlab.com/gitlab-org/gitlab/-/issues/432494

View File

@ -650,24 +650,24 @@ export default {
/>
</div>
</template>
<work-item-detail-modal
v-if="!isModal"
ref="modal"
:work-item-id="modalWorkItemId"
:work-item-iid="modalWorkItemIid"
:work-item-full-path="modalWorkItemNamespaceFullPath"
:show="true"
@close="updateUrl"
@openReportAbuse="toggleReportAbuseDrawer(true, $event)"
/>
<abuse-category-selector
v-if="isReportDrawerOpen"
:reported-user-id="reportedUserId"
:reported-from-url="reportedUrl"
:show-drawer="true"
@close-drawer="toggleReportAbuseDrawer(false)"
/>
</section>
</section>
<work-item-detail-modal
v-if="!isModal"
ref="modal"
:work-item-id="modalWorkItemId"
:work-item-iid="modalWorkItemIid"
:work-item-full-path="modalWorkItemNamespaceFullPath"
:show="true"
@close="updateUrl"
@openReportAbuse="toggleReportAbuseDrawer(true, $event)"
/>
<abuse-category-selector
v-if="isReportDrawerOpen"
:reported-user-id="reportedUserId"
:reported-from-url="reportedUrl"
:show-drawer="true"
@close-drawer="toggleReportAbuseDrawer(false)"
/>
</div>
</template>

View File

@ -143,12 +143,23 @@ $work-item-overview-gap-width: 2rem;
}
}
.work-item-view {
.gl-modal .work-item-view,
.work-item-drawer .work-item-view {
container-name: work-item-view;
container-type: inline-size;
}
@container work-item-view (max-width: #{calc($breakpoint-md - 1px)}) {
@mixin container-and-media($query) {
@container work-item-view (#{$query}) {
@content;
}
@media ($query) {
@content;
}
}
@include container-and-media("max-width: #{calc($breakpoint-md - 0.02px)}") {
.work-item-overview {
display: block !important;
}
@ -176,7 +187,7 @@ $work-item-overview-gap-width: 2rem;
margin-top: $gl-spacing-scale-5;
}
@container work-item-view (min-width: #{$breakpoint-md}) {
@include container-and-media("min-width: #{calc($breakpoint-md)}") {
.work-item-attributes-wrapper {
top: calc(#{$calc-application-header-height} + #{$work-item-sticky-header-height});
height: calc(#{$calc-application-viewport-height} - #{$work-item-sticky-header-height});
@ -190,9 +201,7 @@ $work-item-overview-gap-width: 2rem;
.work-item-date-picker {
max-width: 175px;
}
}
@container work-item-view (min-width: #{$breakpoint-md}) {
.work-item-overview-right-sidebar {
margin-top: 0;
grid-row-start: 1;

View File

@ -65,9 +65,9 @@ class Admin::GroupsController < Admin::ApplicationController
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path,
status: :found,
alert: format(_('Group %{group_name} was scheduled for deletion.'), group_name: @group.name)
flash[:toast] = format(_("Group '%{group_name}' is being deleted."), group_name: @group.full_name)
redirect_to admin_groups_path, status: :found
end
private

View File

@ -43,7 +43,7 @@ class Admin::ProjectsController < Admin::ApplicationController
def destroy
::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:notice] = format(_("Project '%{project_name}' is in the process of being deleted."), project_name: @project.full_name)
flash[:toast] = format(_("Project '%{project_name}' is being deleted."), project_name: @project.full_name)
redirect_to admin_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => e

View File

@ -173,7 +173,9 @@ class GroupsController < Groups::ApplicationController
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
redirect_to root_path, status: :found, alert: "Group '#{@group.name}' was scheduled for deletion."
flash[:toast] = format(_("Group '%{group_name}' is being deleted."), group_name: @group.full_name)
redirect_to root_path, status: :found
end
# rubocop: disable CodeReuse/ActiveRecord

View File

@ -194,7 +194,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
flash[:toast] = format(_("Project '%{project_name}' is being deleted."), project_name: @project.full_name)
redirect_to dashboard_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => e

View File

@ -39,7 +39,7 @@ module BreadcrumbsHelper
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': build_item_list_elements&.map&.with_index do |item, index|
itemListElement: build_item_list_elements&.map&.with_index do |item, index|
{
'@type' => 'ListItem',
'position' => index + 1,

View File

@ -187,11 +187,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(
true,
statistic_icon('rocket-launch') +
n_('%{strong_start}%{release_count}%{strong_end} Release', '%{strong_start}%{release_count}%{strong_end} Releases', releases_count).html_safe % {
(n_('%{strong_start}%{release_count}%{strong_end} Release', '%{strong_start}%{release_count}%{strong_end} Releases', releases_count).html_safe % {
release_count: number_with_delimiter(releases_count),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
}),
project_releases_path(project)
)
end
@ -205,11 +205,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(
true,
statistic_icon('environment') +
n_('%{strong_start}%{count}%{strong_end} Environment', '%{strong_start}%{count}%{strong_end} Environments', environments_count).html_safe % {
(n_('%{strong_start}%{count}%{strong_end} Environment', '%{strong_start}%{count}%{strong_end} Environments', environments_count).html_safe % {
count: number_with_delimiter(environments_count),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
}),
project_environments_path(project)
)
end
@ -218,11 +218,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(
true,
statistic_icon('commit') +
n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
(n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
commit_count: number_with_delimiter(statistics.commit_count),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
}),
empty_repo? ? nil : project_commits_path(project, default_branch_or_main)
)
end
@ -231,11 +231,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(
true,
statistic_icon('branch') +
n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % {
(n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % {
branch_count: number_with_delimiter(repository.branch_count),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
}),
empty_repo? ? nil : project_branches_path(project)
)
end
@ -252,11 +252,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(
true,
statistic_icon('terraform') +
n_('%{strong_start}%{terraform_states_count}%{strong_end} Terraform State', '%{strong_start}%{terraform_states_count}%{strong_end} Terraform States', project.terraform_states.count).html_safe % {
(n_('%{strong_start}%{terraform_states_count}%{strong_end} Terraform State', '%{strong_start}%{terraform_states_count}%{strong_end} Terraform States', project.terraform_states.count).html_safe % {
terraform_states_count: number_with_delimiter(project.terraform_states.count),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
} + terraform_warn_icon,
}) + terraform_warn_icon,
project_terraform_index_path(project)
)
end
@ -266,11 +266,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData.new(
true,
statistic_icon('label') +
n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % {
(n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % {
tag_count: number_with_delimiter(repository.tag_count),
strong_start: '<strong class="project-stat-value">'.html_safe,
strong_end: '</strong>'.html_safe
},
}),
empty_repo? ? nil : project_tags_path(project)
)
end

View File

@ -34,12 +34,12 @@ module CloudSeed
current_user.id,
project.id,
{
'google_oauth2_token': google_oauth2_token,
'gcp_project_id': gcp_project_id,
'instance_name': instance_name,
'database_version': database_version,
'environment_name': environment_name,
'is_protected': protected?
google_oauth2_token: google_oauth2_token,
gcp_project_id: gcp_project_id,
instance_name: instance_name,
database_version: database_version,
environment_name: environment_name,
is_protected: protected?
}
)
end

View File

@ -57,7 +57,7 @@ class CohortsService
month_totals =
all_months
.map { |activity_month| counts_by_month[[registration_month, activity_month]] }
.reduce([]) { |result, total| result << result.last.to_i + total.to_i }
.reduce([]) { |result, total| result << (result.last.to_i + total.to_i) }
.reverse
overall_total = month_totals.first

View File

@ -18,7 +18,7 @@ module ValidatesClassificationLabel
def rejection_reason_for_label(label)
reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label }
reason_from_service || (_("Access to '%{classification_label}' not allowed") % { classification_label: label })
end
def classification_label_change?(record, attribute_name)

View File

@ -4,7 +4,7 @@ module Groups
module GroupLinks
class DestroyService < ::Groups::BaseService
def execute(one_or_more_links, skip_authorization: false)
unless skip_authorization || group && can?(current_user, :admin_group_member, group)
unless skip_authorization || (group && can?(current_user, :admin_group_member, group))
return error('Not Found', 404)
end

View File

@ -79,10 +79,10 @@ module IssuableLinks
link = relate_issuables(referenced_object)
if link.errors.any?
@errors << _("%{ref} cannot be added: %{error}") % {
@errors << (_("%{ref} cannot be added: %{error}") % {
ref: referenced_object.to_reference,
error: link.errors.messages.values.flatten.to_sentence
}
})
else
after_create_for(link)
end

View File

@ -161,7 +161,7 @@ module Issues
end
def start_position
@start_position ||= (RelativePositioning::START_POSITION - (gaps / 2) * gap_size).to_i
@start_position ||= (RelativePositioning::START_POSITION - ((gaps / 2) * gap_size)).to_i
end
def with_retry(initial_batch_size, exit_batch_size)

View File

@ -83,7 +83,7 @@ module Jira
def error_message(error)
reportable_error_message(error) ||
s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
(s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe })
end
# Returns a user-facing error message if possible, otherwise `nil`.
@ -113,7 +113,7 @@ module Jira
when 'Forbidden'
s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
when 'Bad Request'
jira_ruby_json_error_message(error.response.body) || s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
jira_ruby_json_error_message(error.response.body) || (s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe })
end
end

View File

@ -938,7 +938,7 @@ Settings['sidekiq']['routing_rules'] = Settings.build_sidekiq_routing_rules(Sett
# GitLab Shell
#
Settings['gitlab_shell'] ||= {}
Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || Settings.gitlab['user_home'] + '/gitlab-shell/')
Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || (Settings.gitlab['user_home'] + '/gitlab-shell/'))
Settings.gitlab_shell['hooks_path'] = :deprecated_use_gitlab_shell_path_instead
Settings.gitlab_shell['authorized_keys_file'] ||= File.join(Dir.home, '.ssh', 'authorized_keys')
Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret')

View File

@ -47,7 +47,7 @@ module CarrierWave
# avoid a get by using local references
local_directory = connection.directories.new(key: @uploader.fog_directory)
local_file = local_directory.files.new(key: path)
expire_at = options[:expire_at] || ::Fog::Time.now + @uploader.fog_authenticated_url_expiration
expire_at = options[:expire_at] || (::Fog::Time.now + @uploader.fog_authenticated_url_expiration)
case @uploader.fog_credentials[:provider]
when 'AWS', 'Google', 'AzureRM'
local_file.url(expire_at, options)

View File

@ -18,7 +18,7 @@ module Kaminari
# Total count has to be 0 if loaded records are 0
return @total_count = 0 if (current_page == 1) && @records.empty?
# Total count is calculable at the last page
return @total_count = (current_page - 1) * limit_value + @records.length if @records.any? && (@records.length < limit_value)
return @total_count = ((current_page - 1) * limit_value) + @records.length if @records.any? && (@records.length < limit_value)
end
limit = options.fetch(:limit, MAX_COUNT_LIMIT).to_i

View File

@ -90,7 +90,7 @@
release: 15.0 # XX.Y
- name: Limited Availability GitLab SaaS runners on macOS (x86-64) # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
GitLab SaaS runners on macOS are now in [Limited Availability](https://about.gitlab.com/handbook/product/gitlab-the-product/#limited-availability-la). If you use GitLab SaaS and have a Premium or Ultimate subscription, you can build applications that require macOS in a secure, on-demand GitLab Runner build environment that's fully integrated with GitLab CI/CD. As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html#cost-factor) of 6.
GitLab SaaS runners on macOS are now in [Limited Availability](https://about.gitlab.com/handbook/product/gitlab-the-product/#limited-availability-la). If you use GitLab SaaS and have a Premium or Ultimate subscription, you can build applications that require macOS in a secure, on-demand GitLab Runner build environment that's fully integrated with GitLab CI/CD. As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/compute_minutes.html#cost-factor) of 6.
stage: verify # String value of the stage that the feature was created in. e.g., Growth
self-managed: false # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)

View File

@ -36,7 +36,7 @@
[Supply-chain Levels for Software Artifacts (SLSA)](https://github.com/slsa-framework/slsa) is a security framework that helps ensure the security and integrity of your software supply chain. By default, GitLab Runner is now capable of generating and producing SLSA-2 compliant attestation metadata for build artifacts.
If the artifact is stored in a registry, then the attestation metadata is stored alongside the artifact in that registry. Otherwise, the metadata is in rendered in a plain text `.json` file that's stored with the artifact.
This new attestation information can help you more easily verify that your build artifacts have not been tampered with. To enable this feature, simply set `RUNNER_GENERATE_ARTIFACTS_METADATA = "true"` in your `.gitlab-ci.yml` file.
As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html#cost-factor) of 6.
As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/compute_minutes.html#cost-factor) of 6.
stage: verify
self-managed: true
gitlab-com: true

View File

@ -16,7 +16,7 @@
- name: Upsizing GitLab SaaS runners on Linux
description: | # Do not modify this line, instead modify the lines below.
You asked, we listened! In our efforts to be best-in-class for CI/CD build speeds, we're doubling the vCPU & RAM for all GitLab SaaS runners on Linux, with no increase in the [cost factor](https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html#cost-factor).
You asked, we listened! In our efforts to be best-in-class for CI/CD build speeds, we're doubling the vCPU & RAM for all GitLab SaaS runners on Linux, with no increase in the [cost factor](https://docs.gitlab.com/ee/ci/pipelines/compute_minutes.html#cost-factor).
We're excited to see pipelines run faster and boost productivity.
stage: Verify

View File

@ -0,0 +1,9 @@
---
migration_job_name: BackfillRelatedEpicLinksToIssueLinks
description: Creates a record on issue_links table for every related_epic_links record
feature_category: team_planning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152104
milestone: '17.1'
queued_migration_version: 20240517131715
finalize_after: '2024-06-13'
finalized_by: # version of the migration that finalized this BBM

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class QueueBackfillRelatedEpicLinksToIssueLinks < Gitlab::Database::Migration[2.2]
milestone '17.0'
restrict_gitlab_migration gitlab_schema: :gitlab_main
MIGRATION = "BackfillRelatedEpicLinksToIssueLinks"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 1000
SUB_BATCH_SIZE = 100
def up
queue_batched_background_migration(
MIGRATION,
:related_epic_links,
:id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(MIGRATION, :related_epic_links, :id, [])
end
end

View File

@ -0,0 +1 @@
672eaf2d05154e166aef1d7d9b13100bb33d599172926eb37d07b69933bc33bc

View File

@ -1,7 +1,7 @@
---
status: ongoing
creation-date: "2023-11-23"
authors: [ "@Andysoiron", "@g.hickman" ]
authors: [ "@Andysoiron", "@g.hickman", "@mcavoj" ]
coach: "@fabiopitino"
approvers: [ "@g.hickman" ]
owning-stage: "~devops::govern"
@ -53,11 +53,14 @@ Known compliance pipelines issues are:
## Proposal
Currently, security policies can include multiple scan actions. Each scan action will result in a CI job that will be injected in the project CI pipeline.
The new feature allows you to define custom CI jobs that will be injected into the project CI pipeline as well. We want to generalize the security policy
The new policy type Pipeline Execution Policy allows users to define custom CI jobs that will be injected into the project CI pipeline as well. We want to generalize the security policy
approach to provide the same flexibility that [compliance framework](../../../user/group/compliance_frameworks.md) needs. The combination of the 2 features
means that security policies can be scope to compliance frameworks and enforce the presence of custom CI jobs.
Like scan execution policies, custom CI jobs can be scoped to certain branch names, branch types or compliance frameworks applied to the project.
Like Scan Execution Policies, Pipeline Execution Policy jobs can be
[scoped](../../../user/application_security/policies/scan-execution-policies.md#policy_scope-scope-type)
to certain compliance frameworks applied to the project.
It should be possible to control when the policy jobs are enforced by using the existing [workflow rules](../../../ci/yaml/workflow.md).
Users can leverage one of the predefined security-policy stages to position jobs in the pipeline according to their needs.
Transitioning from compliance pipelines to the new feature should be as smooth as possible.
@ -67,25 +70,25 @@ Transitioning from compliance pipelines to the new feature should be as smooth a
The Pipeline Execution Policy MVC will allow the transition from compliance pipelines.
- It should be possible to add custom CI YAML to a security policy. The CI YAML should follow the same schema as `.gitlab-ci.yml` files. The custom CI will be merged with the project CI when a pipeline starts.
- It should be possible to add custom CI YAML to a security policy. For simplicity, the custom CI YAML should support the same configuration and follow the same schema as `.gitlab-ci.yml` files. The custom CI will be merged with the project CI when a pipeline starts.
- The security policy schema should allow the custom CI to be defined in an external file by allowing a project and file path to be added.
- Scan execution policies should execute custom CI YAML similar to existing policies, by injecting the job into the GitLab CI YAML of the target projects.
- At minimum, pipeline execution policy jobs should align with [existing CI variable precedence](../../../ci/variables/index.md#cicd-variable-precedence). Ideally, all CI variables defined in a scan execution policy job should execute as highest precedence. Specifically, scan execution job variables should take precedence over project, group, and instance variables compliance project variables, among others.
- Jobs should be executed in a way that is visible to users within the pipeline and that will not allow project jobs to override the SEP jobs. In scan execution policies today, we utilize the index pattern (-0,-1,-2,...) to increment the name of the job if a job of the same name exists. This also gives some minor indication of which jobs are executed by a security policy. For custom YAML jobs, the same pattern should be utilized.
- Users should be able to define the stage in which their job will run, and scan execution policies will have a method to handle precedence. For example, a security/compliance team may define want to enforce jobs that run commonly after a build stage. They would be able to use build_after (for example) and scan execution policies would inject the build_after stage after the build stage and enforce the custom CI YAML defined in the pipeline execution policy within this stage. The stage and job cannot be interfered with by development teams once enforced by a scan execution policy. We should define the rules that allow for injecting stages cleanly into all enforced projects but be minimal invasive as to the project CI.
- Pipeline execution policies should execute custom CI YAML in projects that do not contain an existing CI configuration, the same as standard scan execution policies work today.
- Pipeline Execution Policies should execute custom CI YAML by creating jobs in isolated pipelines which are merged into the pipeline of the target projects.
- At minimum, Pipeline Execution Policy jobs should align with [existing CI variable precedence](../../../ci/variables/index.md#cicd-variable-precedence).
Ideally, Pipeline Execution Policy jobs should not get any user-defined variables except those defined in the group or project where the policy belongs.
- Jobs should be executed in a way that is visible to users within the pipeline and that will not allow project jobs to override the policy jobs. In Scan Execution Policies today, we utilize the index pattern (-0,-1,-2,...) to increment the name of the job if a job of the same name exists. This also gives some minor indication of which jobs are executed by a security policy. For Pipeline Execution Policy jobs, the same pattern should be utilized.
- Jobs coming from the policies should be marked as such in the database so that they can be distinguished, for example by using build metadata. This allows for different handling of jobs and the corresponding variables by the runner.
- Users should be able to define the stage in which their job will run, and Pipeline Execution Policies will have a method to handle precedence. For example, a security/compliance team may want to enforce jobs that run commonly after a build stage. The stages and jobs must not interfere with those defined by development teams once enforced by a Pipeline Execution Policy.
- Pipeline Execution Policies should allow for jobs to be enforced in projects that do not contain an existing CI configuration.
### Stages management strategy
We want users to be able to place jobs to run before or after certain CI stages of the project pipeline.
To achieve this, we want to introduce 3 reserved stages that can be used only by pipeline execution policies and injected into the project pipeline:
To achieve this, we want to introduce 2 reserved stages that can be used only by pipeline execution policies and injected into the project pipeline:
1. `.pipeline-policy-pre` stage will run at the very beginning of the pipeline, before the `.pre` stage.
1. `.pipeline-policy` stage will be injected after the `test` stage. If the `test` stage does not exist, it will be injected after the `build` stage. If the `build` stage does not exist, it will be injected at the beginning of the pipeline after the `.pre` stage.
1. `.pipeline-policy-post` stage will run at the very end of the pipeline, after the `.post` stage.
Injecting jobs in any of these 3 stages is guaranteed to always work. Execution policy jobs can also be assigned to any stage that exists in the project pipeline. In this case, however, it's not guaranteed that the injection always works as it depends whether the project pipeline has declared such stage.
It will not be possible to create custom stages in a pipeline execution policy.
Injecting jobs in any of these stages is guaranteed to always work. Execution policy jobs can also be assigned to any standard (`build`, `test`, `deploy`) or user-declared stages. However, in this case, the jobs may be ignored depending on the project pipeline configuration.
We will try this approach as part of the experiment phase. We also discussed the following strategies that we might want to try:
@ -133,4 +136,5 @@ If the `test` stage doesn't exist, it will be injected after the `build` stage.
## Links
- [Pipeline execution policy MVC epic](https://gitlab.com/groups/gitlab-org/-/epics/7312)
- [Pipeline Execution Policy Type](https://gitlab.com/groups/gitlab-org/-/epics/13266#top)
- [Pipeline Execution Action (Custom CI YAML Support) for Scan Execution Policy Type](https://gitlab.com/groups/gitlab-org/-/epics/7312)

View File

@ -35,6 +35,43 @@ We are in the process of migrating our CSS utility class setup to [Tailwind CSS]
See the [Tailwind CSS blueprint](../../../architecture/blueprints/tailwindcss/index.md) for motivation, proposal,
and implementation details.
#### Tailwind CSS basics
Below are some Tailwind CSS basics and information about how it has been configured to use the [Pajamas design system](https://design.gitlab.com/). For a more in-depth guide see the [official Tailwind CSS documentation](https://tailwindcss.com/docs/utility-first).
##### Prefix
We have configured Tailwind CSS to use a [prefix](https://tailwindcss.com/docs/configuration#prefix) so all utility classes are prefixed with `gl-`.
When using responsive utilities or state modifiers the prefix goes after the colon.
**Examples:** `gl-mt-5`, `lg:gl-mt-5`.
##### Responsive CSS utility classes
[Responsive CSS utility classes](https://tailwindcss.com/docs/responsive-design) are prefixed with the breakpoint name, followed by the `:` character.
The available breakpoints are configured in [tailwind.defaults.js#L44](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js#L44)
**Example:** `lg:gl-mt-5`
##### Hover, focus, and other state modifiers
[State modifiers](https://tailwindcss.com/docs/hover-focus-and-other-states) can be used to conditionally apply any Tailwind CSS class. Prefix the CSS utility class with the name of the modifier, followed by the `:` character.
**Example:** `hover:gl-underline`
##### `!important` modifier
You can use the [important modifier](https://tailwindcss.com/docs/configuration#important-modifier) by adding `!` to the beginning of the CSS utility class. When using in conjunction with responsive utility classes or state modifiers the `!` goes after the `:` character.
**Examples:** `!gl-mt-5`, `lg:!gl-mt-5`, `hover:!gl-underline`
##### Spacing and sizing CSS utility classes
Spacing and sizing CSS utility classes (e.g. `margin`, `padding`, `width`, `height`) use our spacing scale defined in
[tailwind.defaults.js#L4](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js#L4). They will use the naming conventions documented in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) but the scale will not match. When using the [Tailwind CSS autocomplete](#tailwind-css-autocomplete) our configured spacing scale will be shown.
**Example:** `gl-mt-5` will be `margin-top: 1rem;`
##### Color CSS utility classes
Color CSS utility classes (e.g. `color` and `background-color`) use colors defined in [src/tokens/build/tailwind/tokens.cjs](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/24a08b50da6bd3d34fb3f8d24f84436d90d165f6/src/tokens/build/tailwind/tokens.cjs). They will use the naming conventions documented in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) but the color names will not match. When using the [Tailwind CSS autocomplete](#tailwind-css-autocomplete) our configured colors will be shown.
**Example:** `gl-text-red-500` will be `color: var(--red-500, #dd2b0e);`
#### Building the Tailwind CSS bundle
When using Vite or Webpack with the GitLab Development Kit, Tailwind CSS watches for file changes to
@ -92,7 +129,7 @@ For full HAML and custom `*-class` prop support these are the recommended update
#### Official Tailwind CSS documentation
GitLab defines its own Tailwind CSS config in [https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js) to match the Pajamas design system and to prefix CSS utility classes with `gl-`. This means that in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) the spacing, sizing, and color CSS utility classes may not match. Also, the `gl-` prefix will not be shown. Here is our [spacing scale](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js#L51) and [colors](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js#L8). In the future we plan to utilize [Tailwind config viewer](https://github.com/rogden/tailwind-config-viewer) to have a Tailwind CSS documentation site specific to GitLab.
GitLab defines its own Tailwind CSS config in [tailwind.defaults.js](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js) to match the Pajamas design system and to prefix CSS utility classes with `gl-`. This means that in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) the spacing, sizing, and color CSS utility classes may not match. Also, the `gl-` prefix will not be shown. Here is our [spacing scale](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js#L4) and [colors](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/24a08b50da6bd3d34fb3f8d24f84436d90d165f6/src/tokens/build/tailwind/tokens.cjs). In the future we plan to utilize [Tailwind config viewer](https://github.com/rogden/tailwind-config-viewer) to have a Tailwind CSS documentation site specific to GitLab.
### Where should you put new utility classes?

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class BackfillRelatedEpicLinksToIssueLinks < BatchedMigrationJob
operation_name :backfill_issue_links_with_related_epic_links
feature_category :team_planning
def perform
each_sub_batch do |sub_batch|
values_subquery = sub_batch.select(select_fields_to_insert_sql)
values_subquery = values_subquery.joins(joins_target_and_source_epic_sql)
connection.execute(<<~SQL)
INSERT INTO issue_links (source_id, target_id, link_type, created_at, updated_at)
#{values_subquery.to_sql}
ON CONFLICT (source_id, target_id)
DO UPDATE SET
link_type = EXCLUDED.link_type,
created_at = EXCLUDED.created_at,
updated_at = EXCLUDED.updated_at
SQL
end
end
private
def select_fields_to_insert_sql
<<~SQL
source_epics.issue_id AS source_id,
target_epics.issue_id AS target_id,
related_epic_links.link_type,
related_epic_links.created_at AT TIME ZONE '#{Time.zone.tzinfo.name}' AS created_at,
related_epic_links.updated_at AT TIME ZONE '#{Time.zone.tzinfo.name}' AS updated_at
SQL
end
def joins_target_and_source_epic_sql
<<~SQL
INNER JOIN epics source_epics ON related_epic_links.source_id = source_epics.id
INNER JOIN epics target_epics ON related_epic_links.target_id = target_epics.id
SQL
end
end
end
end

View File

@ -361,8 +361,8 @@ module Gitlab
clusters: distinct_count(::Clusters::Cluster.where(time_period), :user_id),
clusters_integrations_prometheus: cluster_integrations_user_distinct_count(::Clusters::Integrations::Prometheus, time_period),
operations_dashboard_default_dashboard: count(::User.active.with_dashboard('operations').where(time_period),
start: minimum_id(User),
finish: maximum_id(User)),
start: minimum_id(User),
finish: maximum_id(User)),
projects_with_error_tracking_enabled: distinct_count(::Project.with_enabled_error_tracking.where(time_period), :creator_id),
projects_with_incidents: distinct_count(::Issue.with_issue_type(:incident).where(time_period), :project_id),
# We are making an assumption here that all alert_management_alerts are associated with an issue of type

View File

@ -95,7 +95,7 @@ module Gitlab
property: MR_APPROVE_ACTION,
label: 'redis_hll_counters.code_review.i_code_review_user_approve_mr_monthly',
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
event: MR_APPROVE_ACTION).to_context]
event: MR_APPROVE_ACTION).to_context]
)
end

View File

@ -51,7 +51,7 @@ module Peek
duration = (call[:duration] * 1000).round(3)
call.merge(duration: duration,
warnings: warning_for(duration, self.class.thresholds[:individual_call]))
warnings: warning_for(duration, self.class.thresholds[:individual_call]))
end
def warning_for(actual, threshold, label: nil)

View File

@ -20,7 +20,7 @@ module Peek
cmd = call[:commands].map { |command| command.join(' ') }.join(', ')
super.merge(cmd: cmd,
instance: call[:storage])
instance: call[:storage])
end
def format_command(cmd)

View File

@ -36,12 +36,12 @@ namespace :gems do
user_id = File.stat(vendor_gem_dir).uid
Kernel.system('docker', 'run',
"--user=#{user_id}", '--rm', "--volume=#{vendor_gem_dir}:/code", docker_image,
'generate',
'--input-spec', api_url,
'--generator-name', 'ruby',
'--output', "/code/#{gem_name}",
"--additional-properties=moduleName=#{module_name}"
"--user=#{user_id}", '--rm', "--volume=#{vendor_gem_dir}:/code", docker_image,
'generate',
'--input-spec', api_url,
'--generator-name', 'ruby',
'--output', "/code/#{gem_name}",
"--additional-properties=moduleName=#{module_name}"
)
end

View File

@ -55,9 +55,9 @@ namespace :gitlab do
end
puts YAML.dump('controller_actions' => controller_actions,
'api_endpoints' => endpoints,
'sidekiq_workers' => workers,
'database_tables' => database_tables)
'api_endpoints' => endpoints,
'sidekiq_workers' => workers,
'database_tables' => database_tables)
end
private

View File

@ -37,7 +37,7 @@ namespace :gitlab do
puts "\nSeeding issues for the '#{project.full_path}' project"
seeder = Quality::Seeders::Issues.new(project: project)
issues_created = seeder.seed(backfill_weeks: args.backfill_weeks.to_i,
average_issues_per_week: args.average_issues_per_week.to_i)
average_issues_per_week: args.average_issues_per_week.to_i)
puts "\n#{issues_created} issues created!"
end
end

View File

@ -19,7 +19,7 @@ namespace :gitlab do
namespace :seed do
desc 'Seed groups with sub-groups/projects/runners/jobs for Runner Fleet testing'
task :runner_fleet,
[:username, :registration_prefix, :runner_count, :job_count] => :gitlab_environment do |_t, args|
[:username, :registration_prefix, :runner_count, :job_count] => :gitlab_environment do |_t, args|
timings = Benchmark.measure do
projects_to_runners = Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder.new(
Gitlab::AppLogger,

View File

@ -12,10 +12,10 @@ namespace :gitlab do
sanitizer = Gitlab::Sanitizers::Exif.new(logger: logger)
sanitizer.batch_clean(start_id: args.start_id, stop_id: args.stop_id,
dry_run: args.dry_run != 'false',
sleep_time: args.sleep_time.to_f,
uploader: args.uploader,
since: args.since)
dry_run: args.dry_run != 'false',
sleep_time: args.sleep_time.to_f,
uploader: args.uploader,
since: args.since)
end
end
end

View File

@ -9212,7 +9212,7 @@ msgstr ""
msgid "BranchRules|Update target branch"
msgstr ""
msgid "BranchRules|Users"
msgid "BranchRules|Users & groups"
msgstr ""
msgid "BranchRules|View details"
@ -24685,9 +24685,6 @@ msgstr ""
msgid "Group %{group_name} was exported successfully."
msgstr ""
msgid "Group %{group_name} was scheduled for deletion."
msgstr ""
msgid "Group %{group_name} was successfully created."
msgstr ""
@ -24697,6 +24694,9 @@ msgstr ""
msgid "Group '%{group_name}' has been successfully restored."
msgstr ""
msgid "Group '%{group_name}' is being deleted."
msgstr ""
msgid "Group '%{group_name}' was successfully updated."
msgstr ""
@ -33148,6 +33148,9 @@ msgstr ""
msgid "MlModelRegistry|Create model version & import artifacts"
msgstr ""
msgid "MlModelRegistry|Create model, version & import artifacts"
msgstr ""
msgid "MlModelRegistry|Creating models is also possible through the MLflow client. %{linkStart}Follow the documentation to learn more.%{linkEnd}"
msgstr ""
@ -33178,9 +33181,15 @@ msgstr ""
msgid "MlModelRegistry|Enter some description"
msgstr ""
msgid "MlModelRegistry|Enter some model description"
msgstr ""
msgid "MlModelRegistry|Error creating model version and uploading artifacts. Please try again."
msgstr ""
msgid "MlModelRegistry|Error creating model, version and uploading artifacts. Please try again."
msgstr ""
msgid "MlModelRegistry|Error importing artifact. Please try again."
msgstr ""
@ -33202,18 +33211,27 @@ msgstr ""
msgid "MlModelRegistry|For example 1.0.0"
msgstr ""
msgid "MlModelRegistry|For example my-model"
msgstr ""
msgid "MlModelRegistry|ID"
msgstr ""
msgid "MlModelRegistry|Info"
msgstr ""
msgid "MlModelRegistry|Initial version name. Must be a semantic version."
msgstr ""
msgid "MlModelRegistry|Latest version"
msgstr ""
msgid "MlModelRegistry|Leave empty to auto increment."
msgstr ""
msgid "MlModelRegistry|Leave empty to skip version creation."
msgstr ""
msgid "MlModelRegistry|MLflow run ID"
msgstr ""
@ -33226,6 +33244,15 @@ msgstr ""
msgid "MlModelRegistry|Model deleted successfully"
msgstr ""
msgid "MlModelRegistry|Model has been created but version or artifacts could not be uploaded. Try creating model version."
msgstr ""
msgid "MlModelRegistry|Model name"
msgstr ""
msgid "MlModelRegistry|Model name must not contain spaces or upper case letter."
msgstr ""
msgid "MlModelRegistry|Model performance"
msgstr ""
@ -33277,6 +33304,9 @@ msgstr ""
msgid "MlModelRegistry|Use versions to track performance, parameters, and metadata"
msgstr ""
msgid "MlModelRegistry|Version Description"
msgstr ""
msgid "MlModelRegistry|Version candidates"
msgstr ""
@ -40264,10 +40294,10 @@ msgstr ""
msgid "Project '%{project_name}' has been successfully restored."
msgstr ""
msgid "Project '%{project_name}' is being imported."
msgid "Project '%{project_name}' is being deleted."
msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted."
msgid "Project '%{project_name}' is being imported."
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
@ -44433,12 +44463,6 @@ msgid_plural "Runners|%{highlightStart}%{duration}%{highlightEnd} seconds"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|%{installLinkStart}Upgrade GitLab Runner%{installLinkEnd} to match your GitLab version. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
msgstr ""
msgid "Runners|%{installLinkStart}Upgrade GitLab Runner%{installLinkEnd} to match your GitLab version. This upgrade is highly recommended for this runner and may contain security or compatibilty fixes. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
msgstr ""
msgid "Runners|%{linkStart}Create a new runner%{linkEnd} to get started."
msgstr ""
@ -44464,6 +44488,12 @@ msgid_plural "Runners|%{strongStart}%{count}%{strongEnd} runners will be permane
msgstr[0] ""
msgstr[1] ""
msgid "Runners|%{upgradeLinkStart}Upgrade GitLab Runner%{upgradeLinkEnd} to match your GitLab version. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
msgstr ""
msgid "Runners|%{upgradeLinkStart}Upgrade GitLab Runner%{upgradeLinkEnd} to match your GitLab version. This upgrade is highly recommended for this runner and may contain security or compatibilty fixes. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
msgstr ""
msgid "Runners|1. Configure your Google Cloud project"
msgstr ""

View File

@ -27,7 +27,7 @@ module RuboCop
before_execute = node.descendants[1].source_range
range_to_remove = node.source_range
.with(begin_pos: before_execute.end_pos,
end_pos: upto_including_execute.end_pos)
end_pos: upto_including_execute.end_pos)
corrector.remove(range_to_remove)
end

View File

@ -34,7 +34,7 @@ module RuboCop
return unless ambiguous_page?(node)
add_offense(node.arguments.each_node(:arg).first,
message: MESSAGE % page_object_name(node))
message: MESSAGE % page_object_name(node))
end
private

View File

@ -111,6 +111,7 @@ RSpec.describe Admin::GroupsController, feature_category: :groups_and_projects d
it 'redirects to the admin group path' do
delete :destroy, params: { id: project.group.path }
expect(flash[:toast]).to eq(format(_("Group '%{group_name}' is being deleted."), group_name: group.full_name))
expect(response).to redirect_to(admin_groups_path)
end
end

View File

@ -2,8 +2,8 @@
require 'spec_helper'
RSpec.describe Admin::ProjectsController do
let!(:project) { create(:project, :public) }
RSpec.describe Admin::ProjectsController, feature_category: :groups_and_projects do
let_it_be(:project) { create(:project, :public) }
before do
sign_in(create(:admin))
@ -107,4 +107,13 @@ RSpec.describe Admin::ProjectsController do
end
end
end
describe 'DELETE #destroy' do
it 'redirects to the admin projects path and displays the flash toast' do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(flash[:toast]).to eq(format(_("Project '%{project_name}' is being deleted."), project_name: project.full_name))
expect(response).to redirect_to(admin_projects_path)
end
end
end

View File

@ -698,6 +698,7 @@ RSpec.describe GroupsController, factory_default: :keep, feature_category: :code
it 'redirects to the root path' do
delete :destroy, params: { id: group.to_param }
expect(flash[:toast]).to eq(format(_("Group '%{group_name}' is being deleted."), group_name: group.full_name))
expect(response).to redirect_to(root_path)
end
end

View File

@ -1083,6 +1083,7 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_gitlab_http_status(:found)
expect(flash[:toast]).to eq(format(_("Project '%{project_name}' is being deleted."), project_name: project.full_name))
expect(response).to redirect_to(dashboard_projects_path)
end

View File

@ -416,7 +416,7 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
it 'removes group', :sidekiq_might_not_need_inline do
expect { remove_with_confirm('Delete group', group.path) }.to change { Group.count }.by(-1)
expect(group.members.all.count).to be_zero
expect(page).to have_content "scheduled for deletion"
expect(page).to have_content "is being deleted"
end
end

View File

@ -286,7 +286,7 @@ RSpec.describe 'Project', feature_category: :source_code_management do
it 'deletes a project', :sidekiq_inline do
expect { remove_with_confirm('Delete project', project.path_with_namespace, 'Yes, delete project') }.to change { Project.count }.by(-1)
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
expect(page).to have_content "Project '#{project.full_name}' is being deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty
expect(project.merge_requests).to be_empty

View File

@ -205,6 +205,19 @@ describe('Api', () => {
});
});
describe('groupSubgroups', () => {
it('fetches group subgroups', () => {
const groupId = '54321';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/subgroups`;
const expectedData = [{ id: 7 }];
mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectedData);
return Api.groupSubgroups(groupId).then(({ data }) => {
expect(data).toEqual(expectedData);
});
});
});
describe('inviteGroupMembers', () => {
it('invites a new email address to create a new User and become a Group Member', () => {
const groupId = 1;

View File

@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { IndexMlModels } from '~/ml/model_registry/apps';
import ModelRow from '~/ml/model_registry/components/model_row.vue';
import ModelCreate from '~/ml/model_registry/components/model_create.vue';
import { MODEL_ENTITIES } from '~/ml/model_registry/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
@ -58,7 +59,7 @@ describe('ml/model_registry/apps/index_ml_models', () => {
const findTitleArea = () => wrapper.findComponent(TitleArea);
const findModelCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
const findBadge = () => wrapper.findComponent(GlExperimentBadge);
const findCreateButton = () => wrapper.findByTestId('create-model-button');
const findModelCreate = () => wrapper.findComponent(ModelCreate);
const findActionsDropdown = () => wrapper.findComponent(ActionsDropdown);
const findSearchableList = () => wrapper.findComponent(SearchableList);
@ -99,7 +100,7 @@ describe('ml/model_registry/apps/index_ml_models', () => {
await waitForPromises();
expect(findCreateButton().exists()).toBe(false);
expect(findModelCreate().exists()).toBe(false);
});
});
@ -112,7 +113,7 @@ describe('ml/model_registry/apps/index_ml_models', () => {
await waitForPromises();
expect(findCreateButton().attributes().href).toBe('path/to/create');
expect(findModelCreate().exists()).toBe(true);
});
});
});

View File

@ -0,0 +1,298 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { visitUrl } from '~/lib/utils/url_utility';
import ModelCreate from '~/ml/model_registry/components/model_create.vue';
import ImportArtifactZone from '~/ml/model_registry/components/import_artifact_zone.vue';
import { uploadModel } from '~/ml/model_registry/services/upload_model';
import createModelMutation from '~/ml/model_registry/graphql/mutations/create_model.mutation.graphql';
import createModelVersionMutation from '~/ml/model_registry/graphql/mutations/create_model_version.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createModelResponses, createModelVersionResponses } from '../graphql_mock_data';
Vue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
jest.mock('~/ml/model_registry/services/upload_model', () => ({
uploadModel: jest.fn(),
}));
describe('ModelCreate', () => {
let wrapper;
let apolloProvider;
beforeEach(() => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
afterEach(() => {
apolloProvider = null;
});
const createWrapper = (
createModelResolver = jest.fn().mockResolvedValue(createModelResponses.success),
createModelVersionResolver = jest.fn().mockResolvedValue(createModelVersionResponses.success),
) => {
const requestHandlers = [
[createModelMutation, createModelResolver],
[createModelVersionMutation, createModelVersionResolver],
];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(ModelCreate, {
provide: {
projectPath: 'some/project',
},
apolloProvider,
});
};
const findModalButton = () => wrapper.findByText('Create model');
const findNameInput = () => wrapper.findByTestId('nameId');
const findVersionInput = () => wrapper.findByTestId('versionId');
const findDescriptionInput = () => wrapper.findByTestId('descriptionId');
const findVersionDescriptionInput = () => wrapper.findByTestId('versionDescriptionId');
const findImportArtifactZone = () => wrapper.findComponent(ImportArtifactZone);
const findGlModal = () => wrapper.findComponent(GlModal);
const findGlAlert = () => wrapper.findComponent(GlAlert);
const submitForm = async () => {
findGlModal().vm.$emit('primary', new Event('primary'));
await waitForPromises();
};
describe('Initial state', () => {
beforeEach(() => {
createWrapper();
});
it('renders the modal button', () => {
expect(findModalButton().text()).toBe('Create model');
});
describe('Modal open', () => {
beforeEach(() => {
findModalButton().trigger('click');
});
it('renders the name input', () => {
expect(findNameInput().exists()).toBe(true);
});
it('renders the version input', () => {
expect(findVersionInput().exists()).toBe(true);
});
it('renders the description input', () => {
expect(findDescriptionInput().exists()).toBe(true);
});
it('renders the version description input', () => {
expect(findVersionDescriptionInput().exists()).toBe(true);
});
it('renders the import artifact zone input', () => {
expect(findImportArtifactZone().exists()).toBe(false);
});
it('renders the import artifact zone input with version entered', async () => {
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
await waitForPromises();
expect(findImportArtifactZone().props()).toEqual({
path: null,
submitOnSelect: false,
value: null,
});
});
it('renders the import modal', () => {
expect(findGlModal().props()).toMatchObject({
modalId: 'create-model-modal',
title: 'Create model, version & import artifacts',
size: 'sm',
});
});
it('renders the cancel button in the modal', () => {
expect(findGlModal().props('actionCancel')).toEqual({ text: 'Cancel' });
});
it('renders the create button in the modal', () => {
expect(findGlModal().props('actionPrimary')).toEqual({
attributes: { variant: 'confirm' },
text: 'Create',
});
});
it('does not render the alert by default', () => {
expect(findGlAlert().exists()).toBe(false);
});
});
});
describe('Successful flow with version', () => {
beforeEach(async () => {
createWrapper();
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findDescriptionInput().vm.$emit('input', 'My model description');
findVersionDescriptionInput().vm.$emit('input', 'My version description');
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await submitForm();
});
it('Makes a create model mutation upon confirm', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createModelMutation,
variables: {
projectPath: 'some/project',
name: 'gpt-alice-1',
description: 'My model description',
},
}),
);
});
it('Makes a create model version mutation upon confirm', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createModelVersionMutation,
variables: {
modelId: 'gid://gitlab/Ml::Model/1',
projectPath: 'some/project',
version: '1.0.0',
description: 'My version description',
},
}),
);
});
it('Uploads a file mutation upon confirm', () => {
expect(uploadModel).toHaveBeenCalledWith({
file: null,
importPath: '/api/v4/projects/1/packages/ml_models/1/files/',
});
});
it('Visits the model versions page upon successful create mutation', async () => {
createWrapper();
await submitForm();
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1/versions/1');
});
});
describe('Successful flow without version', () => {
beforeEach(async () => {
createWrapper();
findNameInput().vm.$emit('input', 'gpt-alice-1');
findDescriptionInput().vm.$emit('input', 'My model description');
jest.spyOn(apolloProvider.defaultClient, 'mutate');
await submitForm();
});
it('Visits the model page upon successful create mutation without a version', async () => {
createWrapper();
await submitForm();
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1');
});
});
describe('Failed flow with version', () => {
beforeEach(async () => {
const failedCreateModelVersionResolver = jest
.fn()
.mockResolvedValue(createModelVersionResponses.failure);
createWrapper(undefined, failedCreateModelVersionResolver);
jest.spyOn(apolloProvider.defaultClient, 'mutate');
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findVersionDescriptionInput().vm.$emit('input', 'My version description');
await submitForm();
});
it('Displays an alert upon failed model create mutation', () => {
expect(findGlAlert().text()).toBe('Version is invalid');
});
});
describe('Failed flow with version retried', () => {
beforeEach(async () => {
const failedCreateModelVersionResolver = jest
.fn()
.mockResolvedValueOnce(createModelVersionResponses.failure);
createWrapper(undefined, failedCreateModelVersionResolver);
jest.spyOn(apolloProvider.defaultClient, 'mutate');
findNameInput().vm.$emit('input', 'gpt-alice-1');
findVersionInput().vm.$emit('input', '1.0.0');
findVersionDescriptionInput().vm.$emit('input', 'My retried version description');
await submitForm();
});
it('Displays an alert upon failed model create mutation', async () => {
expect(findGlAlert().text()).toBe('Version is invalid');
await submitForm();
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
expect.objectContaining({
mutation: createModelVersionMutation,
variables: {
modelId: 'gid://gitlab/Ml::Model/1',
projectPath: 'some/project',
version: '1.0.0',
description: 'My retried version description',
},
}),
);
});
});
describe('Failed flow without version', () => {
describe('Mutation errors', () => {
beforeEach(async () => {
const failedCreateModelResolver = jest
.fn()
.mockResolvedValue(createModelResponses.validationFailure);
createWrapper(failedCreateModelResolver);
jest.spyOn(apolloProvider.defaultClient, 'mutate');
findNameInput().vm.$emit('input', 'gpt-alice-1');
await submitForm();
});
it('Displays an alert upon failed model create mutation', () => {
expect(findGlAlert().text()).toBe("Name is invalid, Name can't be blank");
});
it('Displays an alert upon an exception', () => {
expect(findGlAlert().text()).toBe("Name is invalid, Name can't be blank");
});
});
it('Logs to sentry upon an exception', async () => {
const error = new Error('Runtime error');
createWrapper();
jest.spyOn(apolloProvider.defaultClient, 'mutate').mockImplementation(() => {
throw error;
});
findNameInput().vm.$emit('input', 'gpt-alice-1');
await submitForm();
expect(Sentry.captureException).toHaveBeenCalledWith(error);
});
});
});

View File

@ -69,6 +69,7 @@ export const protectionEmptyStatePropsMock = {
export const protectionRowPropsMock = {
title: 'Test title',
users: usersMock,
groups: groupsMock,
accessLevels: accessLevelsMock,
approvalsRequired,
statusCheckUrl: statusChecksRulesMock[0].externalUrl,
@ -106,6 +107,10 @@ export const branchProtectionsMockResponse = {
project: {
id: 'gid://gitlab/Project/1',
__typename: 'Project',
group: {
id: 'gid://gitlab/Group/1',
__typename: 'Group',
},
branchRules: {
__typename: 'BranchRuleConnection',
nodes: [
@ -158,6 +163,10 @@ export const predefinedBranchRulesMockResponse = {
project: {
id: 'gid://gitlab/Project/1',
__typename: 'Project',
group: {
id: 'gid://gitlab/Group/1',
__typename: 'Group',
},
branchRules: {
__typename: 'BranchRuleConnection',
nodes: [

View File

@ -30,7 +30,10 @@ describe('Branch rule protection row', () => {
});
it('renders an avatars-inline component', () => {
expect(findAvatarsInline().props('avatars')).toMatchObject(protectionRowPropsMock.users);
expect(findAvatarsInline().props('avatars')).toMatchObject([
...protectionRowPropsMock.users,
...protectionRowPropsMock.groups,
]);
expect(findAvatarsInline().props('badgeSrOnlyText')).toBe('1 additional user');
});

View File

@ -58,31 +58,24 @@ describe('Branch rule protection', () => {
});
});
it('renders a protection row for users', () => {
it('renders a protection row for users and groups', () => {
expect(findProtectionRows().at(1).props()).toMatchObject({
showDivider: true,
groups: protectionPropsMock.groups,
users: protectionPropsMock.users,
showDivider: true,
title: i18n.usersTitle,
});
});
it('renders a protection row for groups', () => {
expect(findProtectionRows().at(2).props()).toMatchObject({
accessLevels: protectionPropsMock.groups,
showDivider: true,
title: i18n.groupsTitle,
title: i18n.usersAndGroupsTitle,
});
});
it('renders a protection row for status checks', () => {
const statusCheck = protectionPropsMock.statusChecks[0];
expect(findProtectionRows().at(3).props()).toMatchObject({
expect(findProtectionRows().at(2).props()).toMatchObject({
title: statusCheck.name,
showDivider: false,
statusCheckUrl: statusCheck.externalUrl,
});
expect(findProtectionRows().at(4).props('showDivider')).toBe(true);
expect(findProtectionRows().at(3).props('showDivider')).toBe(true);
});
describe('When `isEditAvailable` prop is set to true', () => {

View File

@ -25,6 +25,12 @@ jest.mock('~/rest_api', () => ({
{ name: 'Project 2', id: '2' },
],
}),
getSubgroups: jest.fn().mockResolvedValue({
data: [
{ name: 'Subgroup 1', id: '1' },
{ name: 'Subgroup 2', id: '2' },
],
}),
}));
Vue.use(VueApollo);

View File

@ -60,7 +60,7 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsRepositoryPipeline, fea
end
describe '#run', :clean_gitlab_redis_shared_state do
let(:validation_response) { double(Hash, 'error?': false) }
let(:validation_response) { double(Hash, error?: false) }
before do
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
@ -172,7 +172,7 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsRepositoryPipeline, fea
end
context 'when snippet is invalid' do
let(:validation_response) { double(Hash, 'error?': true) }
let(:validation_response) { double(Hash, error?: true) }
before do
allow_next_instance_of(Repository) do |repository|

View File

@ -61,7 +61,7 @@ RSpec.describe Constraints::GroupUrlConstrainer do
def build_request(path, method = 'GET')
double(:request,
'get?': (method == 'GET'),
get?: (method == 'GET'),
params: { id: path })
end
end

View File

@ -51,7 +51,7 @@ RSpec.describe Constraints::ProjectUrlConstrainer do
def build_request(namespace, project, method = 'GET')
double(:request,
'get?': (method == 'GET'),
get?: (method == 'GET'),
params: { namespace_id: namespace, id: project })
end
end

View File

@ -38,7 +38,7 @@ RSpec.describe Constraints::UserUrlConstrainer do
def build_request(username, method = 'GET')
double(:request,
'get?': (method == 'GET'),
get?: (method == 'GET'),
params: { username: username })
end
end

View File

@ -273,16 +273,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:response) do
[
{
"name": "docker-alpine",
"path": "gitlab-org/build/cng/docker-alpine",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "docker-alpine",
path: "gitlab-org/build/cng/docker-alpine",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
},
{
"name": "git-base",
"path": "gitlab-org/build/cng/git-base",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "git-base",
path: "gitlab-org/build/cng/git-base",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
}
]
end
@ -468,7 +468,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
describe '.deduplicated_size' do
let(:path) { 'foo/bar' }
let(:response) { { 'size_bytes': 555 } }
let(:response) { { size_bytes: 555 } }
let(:registry_enabled) { true }
subject { described_class.deduplicated_size(path) }
@ -685,16 +685,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:client_response_repositories) do
[
{
"name": "docker-alpine",
"path": "gitlab-org/build/cng/docker-alpine",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "docker-alpine",
path: "gitlab-org/build/cng/docker-alpine",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
},
{
"name": "git-base",
"path": "gitlab-org/build/cng/git-base",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "git-base",
path: "gitlab-org/build/cng/git-base",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
}
]
end
@ -707,16 +707,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:client_response_repositories1) do
[
{
"name": "docker-alpine",
"path": "gitlab-org/build/cng/docker-alpine",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "docker-alpine",
path: "gitlab-org/build/cng/docker-alpine",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
},
{
"name": "git-base",
"path": "gitlab-org/build/cng/git-base",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "git-base",
path: "gitlab-org/build/cng/git-base",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
}
]
end
@ -725,16 +725,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
let(:client_response_repositories2) do
[
{
"name": "docker-alpine1",
"path": "gitlab-org/build/cng/docker-alpine",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "docker-alpine1",
path: "gitlab-org/build/cng/docker-alpine",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
},
{
"name": "git-base1",
"path": "gitlab-org/build/cng/git-base",
"created_at": "2022-06-07T12:11:13.633+00:00",
"updated_at": "2022-06-07T14:37:49.251+00:00"
name: "git-base1",
path: "gitlab-org/build/cng/git-base",
created_at: "2022-06-07T12:11:13.633+00:00",
updated_at: "2022-06-07T14:37:49.251+00:00"
}
]
end

View File

@ -78,7 +78,7 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout
end
it 'creates metric definition files' do
described_class.new(args, { 'ee': true }).invoke_all
described_class.new(args, { ee: true }).invoke_all
expect(weekly_metric_definition).to include("key_path" => "redis_hll_counters.test_category.i_test_event_weekly")
expect(weekly_metric_definition["distribution"]).to include('ee')

View File

@ -52,7 +52,7 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_
end
it 'creates a metric definition file using the template' do
described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name, 'ee': true }).invoke_all
described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name, ee: true }).invoke_all
expect(YAML.safe_load(File.read(metric_definition_path))).to eq(sample_metric)
end
end

View File

@ -0,0 +1,120 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillRelatedEpicLinksToIssueLinks, feature_category: :team_planning do
let!(:epic_type_id) { table(:work_item_types).find_by(base_type: 7).id }
let!(:author) { table(:users).create!(username: 'tester', projects_limit: 100) }
let!(:namespace) { table(:namespaces).create!(name: 'my test group1', path: 'my-test-group1') }
let(:epics) { table(:epics) }
let(:issues) { table(:issues) }
let(:related_epic_links) { table(:related_epic_links) }
let(:issue_links) { table(:issue_links) }
let(:start_id) { related_epic_links.minimum(:id) }
let(:end_id) { related_epic_links.maximum(:id) }
# Source epics
let(:source_epic_1) { create_epic_with_work_item(title: 'Epic 1', iid: 1) }
let(:source_epic_2) { create_epic_with_work_item(title: 'Epic 2', iid: 2) }
let(:source_epic_3) { create_epic_with_work_item(title: 'Epic 3', iid: 3) }
let(:source_epic_4) { create_epic_with_work_item(title: 'Epic 4', iid: 4) }
# Target epics
let(:target_epic_1) { create_epic_with_work_item(title: 'Epic 5', iid: 5) }
let(:target_epic_2) { create_epic_with_work_item(title: 'Epic 6', iid: 6) }
let(:target_epic_3) { create_epic_with_work_item(title: 'Epic 7', iid: 7) }
let(:target_epic_4) { create_epic_with_work_item(title: 'Epic 8', iid: 8) }
# Epic links not in sync(without a corresponding issue links record)
let!(:related_epic_link_1) { create_related_epic_link(source: source_epic_1, target: target_epic_1, link_type: 0) }
let!(:related_epic_link_2) { create_related_epic_link(source: source_epic_2, target: target_epic_2, link_type: 1) }
# Epic link in sync
let!(:related_epic_link_3) { create_related_epic_link(source: source_epic_3, target: target_epic_3, link_type: 0) }
let!(:synced_issue_link_1) do
issue_links.create!(
source_id: source_epic_3.issue_id,
target_id: target_epic_3.issue_id,
link_type: 0
)
end
# Epic link in sync but with outdated value
let!(:related_epic_link_4) { create_related_epic_link(source: source_epic_4, target: target_epic_4, link_type: 0) }
let!(:synced_issue_link_2) do
issue_links.create!(
source_id: source_epic_4.issue_id,
target_id: target_epic_4.issue_id,
link_type: 1,
created_at: 1.day.ago,
updated_at: 1.hour.ago
)
end
subject(:migration) do
described_class.new(
start_id: start_id,
end_id: end_id,
batch_table: :related_epic_links,
batch_column: :id,
sub_batch_size: 2,
pause_ms: 0,
connection: ApplicationRecord.connection
)
end
RSpec::Matchers.define :have_synced_issue_link do
match do |epic_link|
source_work_item_id = epics.find(epic_link.source_id).issue_id
target_work_item_id = epics.find(epic_link.target_id).issue_id
issue_links.find_by(
source_id: source_work_item_id,
target_id: target_work_item_id,
link_type: epic_link.link_type
).present?
end
end
it 'backfills data correctly' do
expect do
migration.perform
end.to change { issue_links.count }.from(2).to(4)
.and not_change { synced_issue_link_1.reload }
.and change { synced_issue_link_2.reload.link_type }.from(1).to(0)
.and change { synced_issue_link_2.reload.created_at }.to(related_epic_link_4.reload.created_at)
.and change { synced_issue_link_2.reload.updated_at }.to(related_epic_link_4.reload.updated_at)
expect(related_epic_link_1).to have_synced_issue_link
expect(related_epic_link_2).to have_synced_issue_link
expect(related_epic_link_3).to have_synced_issue_link
expect(related_epic_link_4).to have_synced_issue_link
end
def create_epic_with_work_item(iid:, title:)
wi = issues.create!(
iid: iid,
author_id: author.id,
work_item_type_id: epic_type_id,
namespace_id: namespace.id,
lock_version: 1,
title: title
)
epics.create!(
iid: iid,
title: title,
title_html: title,
group_id: namespace.id,
author_id: author.id,
issue_id: wi.id
)
end
def create_related_epic_link(source:, target:, link_type:)
related_epic_links.create!(
source_id: source.id,
target_id: target.id,
link_type: link_type
)
end
end

View File

@ -113,7 +113,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
end
context 'when job name is empty' do
let(:entry) { described_class.new(config, name: ''.to_sym) }
let(:entry) { described_class.new(config, name: :"") }
it 'reports error' do
expect(entry.errors).to include "job name can't be blank"

View File

@ -7,8 +7,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
let(:config) do
{
'.hidden_job'.to_sym => { script: 'something' },
'.hidden_bridge'.to_sym => { trigger: 'my/project' },
".hidden_job": { script: 'something' },
".hidden_bridge": { trigger: 'my/project' },
regular_job: { script: 'something' },
my_trigger: { trigger: 'my/project' }
}
@ -81,7 +81,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
end
context 'when no visible jobs present' do
let(:config) { { '.hidden'.to_sym => { script: [] } } }
let(:config) { { ".hidden": { script: [] } } }
it 'returns error about no visible jobs defined' do
expect(entry.errors)

View File

@ -56,7 +56,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli
end
context 'when job name is empty' do
let(:entry) { node_class.new(config, name: ''.to_sym) }
let(:entry) { node_class.new(config, name: :"") }
it 'reports error' do
expect(entry.errors).to include "job name can't be blank"

View File

@ -12,13 +12,13 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
context "when there are no URLs provided" do
let(:pa11y) do
{
"total": 1,
"passes": 0,
"errors": 0,
"results": {
total: 1,
passes: 0,
errors: 0,
results: {
"": [
{
"message": "Protocol error (Page.navigate): Cannot navigate to invalid URL"
message: "Protocol error (Page.navigate): Cannot navigate to invalid URL"
}
]
}
@ -39,10 +39,10 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
context "when there are no errors" do
let(:pa11y) do
{
"total": 1,
"passes": 1,
"errors": 0,
"results": {
total: 1,
passes: 1,
errors: 0,
results: {
"http://pa11y.org/": []
}
}.to_json
@ -61,20 +61,20 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
context "when there are errors" do
let(:pa11y) do
{
"total": 1,
"passes": 0,
"errors": 1,
"results": {
total: 1,
passes: 0,
errors: 1,
results: {
"https://about.gitlab.com/": [
{
"code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
"context": "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
"selector": "#main-nav > div:nth-child(1) > a",
"runner": "htmlcs",
"runnerExtras": {}
code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
type: "error",
typeCode: 1,
message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
context: "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
selector: "#main-nav > div:nth-child(1) > a",
runner: "htmlcs",
runnerExtras: {}
}
]
}
@ -96,10 +96,10 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
context "when data is not a valid JSON string" do
let(:pa11y) do
{
"total": 1,
"passes": 1,
"errors": 0,
"results": {
total: 1,
passes: 1,
errors: 0,
results: {
"http://pa11y.org/": []
}
}

View File

@ -10,27 +10,27 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
let(:code_climate) do
[
{
"categories": [
categories: [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
check_name: "argument_count",
content: {
body: ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
location: {
path: "foo.rb",
lines: {
begin: 10,
end: 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
other_locations: [],
remediation_points: 900000,
severity: "major",
type: "issue",
engine_name: "structure"
}
].to_json
end
@ -68,27 +68,27 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
let(:code_climate) do
[
{
"categories": [
categories: [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
check_name: "argument_count",
content: {
body: ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
location: {
path: "foo.rb",
lines: {
begin: 10,
end: 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
other_locations: [],
remediation_points: 900000,
severity: "major",
type: "issue",
engine_name: "structure"
}
]
end
@ -104,34 +104,34 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
let(:code_climate) do
[
{
"type": "Issue",
"check_name": "Rubocop/Metrics/ParameterLists",
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
"fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
"severity": "minor"
type: "Issue",
check_name: "Rubocop/Metrics/ParameterLists",
description: "Avoid parameter lists longer than 5 parameters. [12/5]",
fingerprint: "ab5f8b935886b942d621399aefkaehfiaehf",
severity: "minor"
},
{
"categories": [
categories: [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
check_name: "argument_count",
content: {
body: ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
location: {
path: "foo.rb",
lines: {
begin: 10,
end: 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
other_locations: [],
remediation_points: 900000,
severity: "major",
type: "issue",
engine_name: "structure"
}
].to_json
end
@ -147,27 +147,27 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
let(:code_climate) do
[
{
"categories": [
categories: [
"Complexity"
],
"check_name": "argument_count",
"content": {
"body": ""
check_name: "argument_count",
content: {
body: ""
},
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
"location": {
"path": "foo.rb",
"lines": {
"begin": 10,
"end": 10
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
location: {
path: "foo.rb",
lines: {
begin: 10,
end: 10
}
},
"other_locations": [],
"remediation_points": 900000,
"severity": "major",
"type": "issue",
"engine_name": "structure"
other_locations: [],
remediation_points: 900000,
severity: "major",
type: "issue",
engine_name: "structure"
}
].to_json
end

View File

@ -8,24 +8,24 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
let(:data) do
[
{
"code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
"context": %(<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>),
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
"runner": "htmlcs",
"runnerExtras": {}
code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
type: "error",
typeCode: 1,
message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
context: %(<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>),
selector: "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
runner: "htmlcs",
runnerExtras: {}
},
{
"code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
"type": "error",
"typeCode": 1,
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
"context": %(<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>),
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
"runner": "htmlcs",
"runnerExtras": {}
code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
type: "error",
typeCode: 1,
message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
context: %(<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>),
selector: "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
runner: "htmlcs",
runnerExtras: {}
}
]
end

View File

@ -30,11 +30,11 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
context 'when a required property is missing in the degradation' do
let(:invalid_degradation) do
{
"type": "Issue",
"check_name": "Rubocop/Metrics/ParameterLists",
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
"fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
"severity": "minor"
type: "Issue",
check_name: "Rubocop/Metrics/ParameterLists",
description: "Avoid parameter lists longer than 5 parameters. [12/5]",
fingerprint: "ab5f8b935886b942d621399aefkaehfiaehf",
severity: "minor"
}.with_indifferent_access
end

View File

@ -2815,12 +2815,12 @@ module Gitlab
build1: {
stage: 'build',
script: 'build',
parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': %w[monitoring app1 app2] }] }
parallel: { matrix: [{ PROVIDER: ['aws'], STACK: %w[monitoring app1 app2] }] }
},
test1: {
stage: 'test',
script: 'test',
needs: [{ job: 'build1', parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': ['app1'] }] } }]
needs: [{ job: 'build1', parallel: { matrix: [{ PROVIDER: ['aws'], STACK: ['app1'] }] } }]
}
}
end
@ -3255,7 +3255,7 @@ module Gitlab
end
context 'returns errors if there are no visible jobs defined' do
let(:config) { YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) }
let(:config) { YAML.dump({ before_script: ["bundle update"], ".hidden": { script: 'ls' } }) }
it_behaves_like 'returns errors', 'jobs config should contain at least one visible job'
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillRelatedEpicLinksToIssueLinks, feature_category: :team_planning do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :related_epic_links,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE
)
}
end
end
end

View File

@ -6,19 +6,19 @@ RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integratio
describe '#perform' do
subject(:perform) { described_class.new.perform(pipeline_id, user_id) }
let(:pipeline) { create(:ci_pipeline) }
let_it_be(:pipeline) { create(:ci_pipeline) }
let_it_be(:user) { create(:user) }
before_all do
pipeline.project.add_maintainer(user)
end
context 'when pipeline exists' do
let(:pipeline_id) { pipeline.id }
context 'when user exists' do
let(:user) { create(:user) }
let(:user_id) { user.id }
before do
pipeline.project.add_maintainer(user)
end
it 'retries the pipeline' do
expect(::Ci::Pipeline).to receive(:find_by_id).with(pipeline.id).and_return(pipeline)
expect(pipeline).to receive(:retry_failed).with(having_attributes(id: user_id))
@ -28,7 +28,7 @@ RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integratio
end
context 'when user does not exist' do
let(:user_id) { 1234 }
let(:user_id) { non_existing_record_id }
it 'does not retry the pipeline' do
expect(::Ci::Pipeline).to receive(:find_by_id).with(pipeline_id).and_return(pipeline)
@ -40,8 +40,8 @@ RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integratio
end
context 'when pipeline does not exist' do
let(:pipeline_id) { 1234 }
let(:user_id) { 1234 }
let(:pipeline_id) { non_existing_record_id }
let(:user_id) { user.id }
it 'returns nil' do
expect(perform).to be_nil