Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-11-26 12:34:19 +00:00
parent 33c421eb90
commit 403df26fe3
68 changed files with 1451 additions and 479 deletions

View File

@ -629,7 +629,7 @@ lib/gitlab/checks/**
/doc/administration/operations/gitlab_sshd.md @brendan777
/doc/administration/operations/moving_repositories.md @eread
/doc/administration/package_information/ @axil
/doc/administration/packages/ @marcel.amirault
/doc/administration/packages/ @lyspin
/doc/administration/packages/index.md @phillipwells
/doc/administration/pages/ @msedlakjakubowski
/doc/administration/polling.md @axil
@ -712,11 +712,11 @@ lib/gitlab/checks/**
/doc/api/cluster_agents.md @phillipwells
/doc/api/code_suggestions.md @jglassman1
/doc/api/commits.md @brendan777
/doc/api/container_registry.md @marcel.amirault
/doc/api/container_registry.md @lyspin
/doc/api/custom_attributes.md @msedlakjakubowski
/doc/api/dependencies.md @rdickenson @phillipwells
/doc/api/dependency_list_export.md @rdickenson
/doc/api/dependency_proxy.md @marcel.amirault
/doc/api/dependency_proxy.md @lyspin
/doc/api/deploy_keys.md @phillipwells
/doc/api/deploy_tokens.md @phillipwells
/doc/api/deployments.md @phillipwells
@ -822,7 +822,7 @@ lib/gitlab/checks/**
/doc/api/project_aliases.md @brendan777
/doc/api/project_badges.md @brendan777
/doc/api/project_clusters.md @phillipwells
/doc/api/project_container_registry_protection_rules.md @marcel.amirault
/doc/api/project_container_registry_protection_rules.md @lyspin
/doc/api/project_forks.md @emily.sahlani
/doc/api/project_import_export.md @ashrafkhamis
/doc/api/project_job_token_scopes.md @marcel.amirault
@ -903,9 +903,8 @@ lib/gitlab/checks/**
/doc/ci/examples/ @lyspin
/doc/ci/examples/deployment/ @phillipwells
/doc/ci/examples/semantic-release.md @phillipwells
/doc/ci/gitlab_google_cloud_integration/ @marcel.amirault
/doc/ci/gitlab_google_cloud_integration/ @lyspin
/doc/ci/index.md @lyspin
/doc/ci/interactive_web_terminal/ @rsarangadharan
/doc/ci/jobs/ @marcel.amirault
/doc/ci/jobs/job_artifacts.md @lyspin
/doc/ci/jobs/job_artifacts_troubleshooting.md @lyspin
@ -1054,6 +1053,7 @@ lib/gitlab/checks/**
/doc/user/application_security/secret_detection/ @rdickenson
/doc/user/application_security/security_dashboard/ @rdickenson
/doc/user/application_security/vulnerabilities/ @rdickenson
/doc/user/application_security/vulnerabilities/risk_assessment_data.md @rdickenson @phillipwells
/doc/user/application_security/vulnerability_report/ @rdickenson
/doc/user/asciidoc.md @brendan777
/doc/user/clusters/ @phillipwells
@ -1104,9 +1104,9 @@ lib/gitlab/checks/**
/doc/user/operations_dashboard/ @phillipwells
/doc/user/organization/ @emily.sahlani
/doc/user/packages/ @phillipwells
/doc/user/packages/container_registry/ @marcel.amirault
/doc/user/packages/dependency_proxy/ @marcel.amirault
/doc/user/packages/harbor_container_registry/ @marcel.amirault
/doc/user/packages/container_registry/ @lyspin
/doc/user/packages/dependency_proxy/ @lyspin
/doc/user/packages/harbor_container_registry/ @lyspin
/doc/user/profile/account/ @jglassman1
/doc/user/profile/achievements.md @emily.sahlani
/doc/user/profile/comment_templates.md @aqualls
@ -1137,8 +1137,8 @@ lib/gitlab/checks/**
/doc/user/project/integrations/beyond_identity.md @brendan777
/doc/user/project/integrations/git_guardian.md @brendan777
/doc/user/project/integrations/github.md @lyspin
/doc/user/project/integrations/google_artifact_management.md @marcel.amirault
/doc/user/project/integrations/harbor.md @marcel.amirault
/doc/user/project/integrations/google_artifact_management.md @lyspin
/doc/user/project/integrations/harbor.md @lyspin
/doc/user/project/integrations/matrix.md @sselhorn
/doc/user/project/issue_board.md @msedlakjakubowski
/doc/user/project/issues/ @msedlakjakubowski

View File

@ -1,6 +1,6 @@
---
start-release-environments-security-pipeline:
allow_failure: false
allow_failure: true
extends:
- .release-environments:rules:start-release-environments-security-pipeline
stage: release-environments

View File

@ -687,7 +687,7 @@ gem 'lockbox', '~> 1.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'valid_email', '~> 0.1' # rubocop:todo Gemfile/MissingFeatureCategory
# JSON
gem 'jsonb_accessor', '~> 1.3.10' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'jsonb_accessor', '~> 1.4' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'json', '~> 2.7.2', feature_category: :shared
gem 'json_schemer', '~> 2.3.0', feature_category: :shared
gem 'oj', '~> 3.13.21' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -348,8 +348,8 @@
{"name":"json","version":"2.7.3","platform":"ruby","checksum":"7cc3ccbfc5f83025f5524776ae8581e911772944169666988406679251932bdf"},
{"name":"json-jwt","version":"1.16.6","platform":"ruby","checksum":"ab451f9cd8743cecc4137f4170806046c1d8a6d4ee6e8570e0b5c958409b266c"},
{"name":"json_schemer","version":"2.3.0","platform":"ruby","checksum":"9f1fa173b859ca520f15e9e8d08b0892ffca80b78dd8221feb3e360ff4cdeb35"},
{"name":"jsonb_accessor","version":"1.3.10","platform":"java","checksum":"6630ac69dac46457b03e1352178ed3e2d7ba2d8edb99f2e9b64a0e60cda9ed26"},
{"name":"jsonb_accessor","version":"1.3.10","platform":"ruby","checksum":"670f80a257ae39e3be9233c6a8ef3b03517e06687affe510dfe61237454c58e0"},
{"name":"jsonb_accessor","version":"1.4","platform":"java","checksum":"2c5590d33d89c7b929d5cf38ae3d2c52658bf6f84f03b06ede5c88e9d76f3451"},
{"name":"jsonb_accessor","version":"1.4","platform":"ruby","checksum":"010e087cb843e76b6a624d68af918a8efd6b0ff2ae25b6f0acebdfea45f776ab"},
{"name":"jsonpath","version":"1.1.2","platform":"ruby","checksum":"6804124c244d04418218acb85b15c7caa79c592d7d6970195300428458946d3a"},
{"name":"jwt","version":"2.9.3","platform":"ruby","checksum":"55fd07ccdd64c622d36859748f2290fb9c119ce30b482867504e9f12654d6a65"},
{"name":"kaminari","version":"1.2.2","platform":"ruby","checksum":"c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e"},

View File

@ -1047,9 +1047,9 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
jsonb_accessor (1.3.10)
activerecord (>= 5.0)
activesupport (>= 5.0)
jsonb_accessor (1.4)
activerecord (>= 6.1)
activesupport (>= 6.1)
pg (>= 0.18.1)
jsonpath (1.1.2)
multi_json
@ -2143,7 +2143,7 @@ DEPENDENCIES
js_regex (~> 3.8)
json (~> 2.7.2)
json_schemer (~> 2.3.0)
jsonb_accessor (~> 1.3.10)
jsonb_accessor (~> 1.4)
jwt (~> 2.9.3)
kaminari (~> 1.2.2)
knapsack (~> 4.0.0)

View File

@ -352,8 +352,8 @@
{"name":"json","version":"2.7.3","platform":"ruby","checksum":"7cc3ccbfc5f83025f5524776ae8581e911772944169666988406679251932bdf"},
{"name":"json-jwt","version":"1.16.6","platform":"ruby","checksum":"ab451f9cd8743cecc4137f4170806046c1d8a6d4ee6e8570e0b5c958409b266c"},
{"name":"json_schemer","version":"2.3.0","platform":"ruby","checksum":"9f1fa173b859ca520f15e9e8d08b0892ffca80b78dd8221feb3e360ff4cdeb35"},
{"name":"jsonb_accessor","version":"1.3.10","platform":"java","checksum":"6630ac69dac46457b03e1352178ed3e2d7ba2d8edb99f2e9b64a0e60cda9ed26"},
{"name":"jsonb_accessor","version":"1.3.10","platform":"ruby","checksum":"670f80a257ae39e3be9233c6a8ef3b03517e06687affe510dfe61237454c58e0"},
{"name":"jsonb_accessor","version":"1.4","platform":"java","checksum":"2c5590d33d89c7b929d5cf38ae3d2c52658bf6f84f03b06ede5c88e9d76f3451"},
{"name":"jsonb_accessor","version":"1.4","platform":"ruby","checksum":"010e087cb843e76b6a624d68af918a8efd6b0ff2ae25b6f0acebdfea45f776ab"},
{"name":"jsonpath","version":"1.1.2","platform":"ruby","checksum":"6804124c244d04418218acb85b15c7caa79c592d7d6970195300428458946d3a"},
{"name":"jwt","version":"2.9.3","platform":"ruby","checksum":"55fd07ccdd64c622d36859748f2290fb9c119ce30b482867504e9f12654d6a65"},
{"name":"kaminari","version":"1.2.2","platform":"ruby","checksum":"c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e"},

View File

@ -1061,9 +1061,9 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
jsonb_accessor (1.3.10)
activerecord (>= 5.0)
activesupport (>= 5.0)
jsonb_accessor (1.4)
activerecord (>= 6.1)
activesupport (>= 6.1)
pg (>= 0.18.1)
jsonpath (1.1.2)
multi_json
@ -2170,7 +2170,7 @@ DEPENDENCIES
js_regex (~> 3.8)
json (~> 2.7.2)
json_schemer (~> 2.3.0)
jsonb_accessor (~> 1.3.10)
jsonb_accessor (~> 1.4)
jwt (~> 2.9.3)
kaminari (~> 1.2.2)
knapsack (~> 4.0.0)

View File

@ -95,6 +95,7 @@ export default {
</template>
<template #cell(author)="{ item: { author } }">
<gl-avatar-link
v-if="author"
:href="author.webUrl"
:title="author.name"
class="js-user-link !gl-text-subtle"

View File

@ -161,6 +161,8 @@ export default {
}
},
allowDuplicateExceptions(item) {
if (item.format === NUGET_PACKAGE_FORMAT) return item.duplicatesAllowed;
// We're also enabling the duplicate exceptions input when duplicates are allowed
// But the change is behind the packagesAllowDuplicateExceptions feature flag
return !this.glFeatures.packagesAllowDuplicateExceptions && item.duplicatesAllowed;

View File

@ -41,6 +41,10 @@ export const DESIGN_SINGLE_ARCHIVE_ERROR = s__(
const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped. %{reason}');
const MAX_SKIPPED_FILES_LISTINGS = 5;
export const UPDATE_DESCRIPTION_ERROR = s__(
'DesignManagement|Could not update description. Please try again.',
);
export const UPLOAD_DESIGN_ERROR_MESSAGE = s__(
'DesignManagement|Error uploading a new design. Please try again.',
);

View File

@ -1,39 +1,292 @@
<script>
import { GlButton, GlFormGroup, GlAlert, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { toggleMarkCheckboxes } from '~/behaviors/markdown/utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
import updateDesignDescriptionMutation from '../graphql/update_design_description.mutation.graphql';
import { UPDATE_DESCRIPTION_ERROR } from '../constants';
const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
export default {
i18n: {
descriptionLabel: s__('DesignManagement|Description'),
components: {
MarkdownEditor,
GlAlert,
GlButton,
GlFormGroup,
},
directives: {
SafeHtml,
GlTooltip: GlTooltipDirective,
},
i18n: {
editDescription: s__('DesignManagement|Edit description'),
descriptionLabel: s__('DesignManagement|Description'),
addDescriptionLabel: s__('DesignManagement|Add a description'),
cancelEdit: s__('DesignManagement|Are you sure you want to cancel editing the description?'),
primaryModalButton: s__('DesignManagement|Discard changes'),
cancelModalButton: s__('DesignManagement|Continue editing'),
},
formFieldProps: {
id: 'design-description',
name: 'design-description',
placeholder: s__('DesignManagement|Write a comment or drag your files here…'),
'aria-label': s__('DesignManagement|Design description'),
},
markdownDocsPath: helpPagePath('user/markdown'),
quickActionsDocsPath: helpPagePath('user/project/quick_actions'),
props: {
design: {
type: Object,
required: true,
},
designVariables: {
type: Object,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
},
data() {
return {
descriptionText: this.design.description || '',
showEditor: false,
isSubmitting: false,
errorMessage: '',
autosaveKey: `WorkItem/${getIdFromGraphQLId(this.design.issue.id)}/Design/${getIdFromGraphQLId(
this.design.id,
)}`,
};
},
computed: {
canUpdate() {
return this.design.issue?.userPermissions?.updateDesign && !this.showEditor;
},
showAddDescriptionButton() {
return this.design.issue?.userPermissions?.updateDesign && !this.design.descriptionHtml;
},
},
watch: {
'design.descriptionHtml': {
handler(newDescriptionHtml, oldDescriptionHtml) {
if (newDescriptionHtml !== oldDescriptionHtml) {
this.renderGFM();
}
},
immediate: true,
},
},
methods: {
startEditing() {
this.showEditor = true;
},
closeForm() {
// If description text is empty on cancel,
// restore the old description
if (!this.descriptionText) {
this.descriptionText = this.design.description;
}
this.showEditor = false;
},
async cancelDescription() {
if (this.descriptionText && this.descriptionText !== this.design.description) {
const confirmed = await confirmAction(this.$options.i18n.cancelEdit, {
primaryBtnText: this.$options.i18n.primaryModalButton,
cancelBtnText: this.$options.i18n.cancelModalButton,
primaryBtnVariant: 'danger',
});
if (!confirmed) {
return;
}
// Update markdown's placeholder text based on if
// an existing description was being edited or
// a totally new description was being added
if (!this.design.descriptionHtml) {
this.descriptionText = '';
} else {
this.descriptionText = this.design.description;
}
markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, this.autosaveKey);
this.showEditor = false;
this.enableCheckboxes();
} else {
this.descriptionText = this.design.description;
this.showEditor = false;
}
},
enableCheckboxes() {
if (this.canUpdate) {
const checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
// enable boxes, disabled by default in markdown
checkboxes.forEach((checkbox) => {
// eslint-disable-next-line no-param-reassign
checkbox.disabled = false;
});
}
},
async renderGFM() {
await this.$nextTick();
renderGFM(this.$refs['gfm-content']);
this.enableCheckboxes();
},
setDescriptionText(newText) {
// Do not update when cmd+enter is executed
if (!this.isSubmitting) {
this.descriptionText = newText;
}
},
async updateDesignDescription() {
this.isSubmitting = true;
if (this.$refs.markdownEditor) {
// eslint-disable-next-line @gitlab/require-i18n-strings
trackSavedUsingEditor(this.$refs.markdownEditor.isContentEditorActive, 'Design');
}
try {
const descriptionInput = { description: this.descriptionText, id: this.design.id };
await this.$apollo.mutate({
mutation: updateDesignDescriptionMutation,
variables: {
input: descriptionInput,
},
});
this.closeForm();
} catch (error) {
Sentry.captureException(error);
this.errorMessage = UPDATE_DESCRIPTION_ERROR;
} finally {
this.isSubmitting = false;
}
},
toggleCheckboxes(event) {
const { target } = event;
if (isCheckbox(target)) {
target.disabled = true;
const { sourcepos } = target.parentElement.dataset;
if (!sourcepos) return;
// Toggle checkboxes based on user input
this.descriptionText = toggleMarkCheckboxes({
rawMarkdown: this.descriptionText,
checkboxChecked: target.checked,
sourcepos,
});
// Update the desciption text using mutation
this.updateDesignDescription();
}
},
},
};
</script>
<template>
<div class="design-description-container">
<div class="design-description-view">
<div class="design-description-header gl-mb-2 gl-flex gl-justify-between">
<h3 class="gl-m-0 gl-text-lg !gl-leading-20">
{{ $options.i18n.descriptionLabel }}
</h3>
<gl-form-group
v-if="showEditor"
class="design-description-form common-note-form"
:label="$options.i18n.descriptionLabel"
label-class="!gl-leading-20 !gl-text-lg"
>
<div v-if="errorMessage" class="gl-pb-3">
<gl-alert variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
</div>
<div v-if="design.descriptionHtml" class="design-description js-task-list-container">
<div
ref="gfm-content"
v-safe-html="design.descriptionHtml"
class="md gl-mb-4"
data-testid="design-description-content"
></div>
<markdown-editor
ref="markdownEditor"
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
:form-field-props="$options.formFieldProps"
:quick-actions-docs-path="$options.quickActionsDocsPath"
:autosave-key="autosaveKey"
enable-autocomplete
:supports-quick-actions="false"
autofocus
@input="setDescriptionText"
@keydown.meta.enter="updateDesignDescription"
@keydown.ctrl.enter="updateDesignDescription"
@keydown.esc.stop="cancelDescription"
/>
<div class="gl-mt-3 gl-flex">
<gl-button
category="primary"
variant="confirm"
:loading="isSubmitting"
data-testid="save-description"
@click="updateDesignDescription"
>{{ s__('DesignManagement|Save changes') }}
</gl-button>
<gl-button
category="tertiary"
class="gl-ml-3"
data-testid="cancel"
@click="cancelDescription"
>{{ s__('DesignManagement|Cancel') }}
</gl-button>
</div>
</gl-form-group>
<div v-else class="design-description-view">
<gl-button
v-if="showAddDescriptionButton"
class="gl-mb-5 gl-ml-auto"
size="small"
data-testid="add-design-description"
:aria-label="$options.i18n.addDescriptionLabel"
@click="startEditing"
>
{{ $options.i18n.addDescriptionLabel }}
</gl-button>
<template v-else>
<div class="design-description-header gl-mb-2 gl-flex gl-justify-between">
<h3 class="gl-m-0 gl-text-lg !gl-leading-20">
{{ $options.i18n.descriptionLabel }}
</h3>
<gl-button
v-if="canUpdate"
v-gl-tooltip
class="gl-ml-auto"
size="small"
category="tertiary"
:aria-label="$options.i18n.editDescription"
:title="$options.i18n.editDescription"
data-testid="edit-description"
icon="pencil"
@click="startEditing"
/>
</div>
<div v-if="design.descriptionHtml" class="design-description js-task-list-container">
<div
ref="gfm-content"
v-safe-html="design.descriptionHtml"
class="md gl-mb-4"
data-testid="design-description-content"
@change="toggleCheckboxes"
></div>
</div>
</template>
</div>
</div>
</template>

View File

@ -131,6 +131,9 @@ export default {
design() {
return this.localDesign || {};
},
markdownPreviewPath() {
return `/${this.fullPath}/-/preview_markdown?target_type=Issue`;
},
designVariables() {
return {
fullPath: this.fullPath,
@ -330,8 +333,10 @@ export default {
</div>
<design-sidebar
:design="design"
:design-variables="designVariables"
:is-loading="isLoading"
:is-open="isSidebarOpen"
:markdown-preview-path="markdownPreviewPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@toggleResolvedComments="toggleResolvedComments"
/>

View File

@ -29,6 +29,10 @@ export default {
type: Boolean,
required: true,
},
designVariables: {
type: Object,
required: true,
},
isOpen: {
type: Boolean,
required: true,
@ -37,6 +41,10 @@ export default {
type: Boolean,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
},
data() {
return {
@ -44,8 +52,13 @@ export default {
};
},
computed: {
showDescription() {
return !this.isLoading && Boolean(this.design.descriptionHtml);
showDescriptionForm() {
// user either has permission to add or update description,
// or the existing description should be shown read-only.
return (
!this.isLoading &&
(this.design.issue?.userPermissions?.updateDesign || Boolean(this.design.descriptionHtml))
);
},
discussions() {
return this.design?.discussions ? extractDiscussions(this.design.discussions) : [];
@ -99,7 +112,13 @@ export default {
<design-disclosure :open="isOpen">
<template #default>
<div class="image-notes gl-h-full gl-pt-0" @click.self="handleSidebarClick">
<design-description v-if="showDescription" :design="design" class="gl-border-b gl-my-5" />
<design-description
v-if="showDescriptionForm"
:design="design"
:design-variables="designVariables"
:markdown-preview-path="markdownPreviewPath"
class="gl-border-b gl-my-5"
/>
<div v-if="isLoading" class="gl-my-5">
<gl-skeleton-loader />
</div>

View File

@ -1,5 +1,6 @@
#import "./fragments/design_file.fragment.graphql"
#import "./fragments/discussion_resolved_status.fragment.graphql"
#import "~/graphql_shared/fragments/author.fragment.graphql"
#import "./fragments/design_note.fragment.graphql"
#import "./fragments/version.fragment.graphql"
@ -34,6 +35,21 @@ query getDesignDetails(
}
}
}
issue {
id
title
webPath
webUrl
participants {
nodes {
...Author
}
}
userPermissions {
createDesign
updateDesign
}
}
}
}
versions {

View File

@ -1,5 +1,6 @@
#import "./fragments/design_file.fragment.graphql"
#import "./fragments/discussion_resolved_status.fragment.graphql"
#import "~/graphql_shared/fragments/author.fragment.graphql"
#import "./fragments/design_note.fragment.graphql"
query getLocalDesignQuery($atVersion: DesignManagementVersionID, $filenames: [String!]) {
@ -17,5 +18,20 @@ query getLocalDesignQuery($atVersion: DesignManagementVersionID, $filenames: [St
}
}
}
issue {
id
title
webPath
webUrl
participants {
nodes {
...Author
}
}
userPermissions {
createDesign
updateDesign
}
}
}
}

View File

@ -0,0 +1,11 @@
mutation updateWorkItemDesignDescriptionMutation($input: DesignManagementUpdateInput!) {
designManagementUpdate(input: $input) {
errors
design {
id
image
description
descriptionHtml
}
}
}

View File

@ -235,8 +235,8 @@ class Namespace < ApplicationRecord
.where(project_statistics[:namespace_id].eq(arel_table[:id]))
.lateral(subquery.name)
select(arel_table[Arel.star], subquery[Arel.star])
.from([arel.as('namespaces'), statistics])
model.select(arel_table[Arel.star], subquery[Arel.star])
.from([arel.as(arel_table.name), statistics])
end
scope :with_jira_installation, ->(installation_id) do

View File

@ -8,8 +8,7 @@ module Packages
ExtractionError = Class.new(StandardError)
def execute
return ServiceResponse.success if Namespace::PackageSetting.duplicates_allowed?(existing_package)
return ServiceResponse.success unless target_package_is_duplicate?
return ServiceResponse.success if package_settings_allow_duplicates? || !target_package_is_duplicate?
ServiceResponse.error(
message: 'A package with the same name and version already exists',
@ -21,12 +20,21 @@ module Packages
private
def package_settings_allow_duplicates?
package_settings.nuget_duplicates_allowed? || package_settings.class.duplicates_allowed?(existing_package)
end
def target_package_is_duplicate?
existing_package.name.casecmp(metadata[:package_name]) == 0 &&
(existing_package.version.casecmp(metadata[:package_version]) == 0 ||
existing_package.normalized_nuget_version&.casecmp(metadata[:package_version]) == 0)
end
def package_settings
project.namespace.package_settings
end
strong_memoize_attr :package_settings
def existing_package
::Packages::Nuget::PackageFinder
.new(

View File

@ -27,6 +27,10 @@
"glm_source": {
"description": "GitLab link management value for source on the link the user clicks to start onboarding",
"type": "string"
},
"joining_project": {
"description": "Setting to understand if a user is joining a project or not during onboarding",
"type": "boolean"
}
},
"additionalProperties": false

View File

@ -0,0 +1,10 @@
---
name: api_request_access_with_scope
description: A susbset of API requests authenticated by a token with an audited scope
introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/499461
introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548
feature_category: duo_workflow
milestone: '17.7'
saved_to_database: true
scope: [User]
streamed: true

16
config/events/failed.yml Normal file
View File

@ -0,0 +1,16 @@
---
description: Tracks when a CI build fails pipeline fails. Includes information about build_id, build_artifact_types, exit_code, failure_reason and project_id
action: failed
identifiers:
- project
- user
product_group: pipeline_execution
milestone: '15.7'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92637
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

22
config/events/promote.yml Normal file
View File

@ -0,0 +1,22 @@
---
description: Tracks when an issue is promoted to an epic
action: promote
identifiers:
- project
- namespace
- user
product_group: product_planning
milestone: '14.0'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167137
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate
additional_properties:
property:
description: Always 'issue_id'
value:
description: Issue id

View File

@ -0,0 +1,15 @@
---
description: Tracks when a manifest is pulled from the package registry
action: pull_manifest
identifiers:
- namespace
- user
product_group: container_registry
milestone: '14.2'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167137
distributions:
- ce
tiers:
- free
- premium
- ultimate

View File

@ -0,0 +1,17 @@
---
description: Tracks when user is reported for abuse
action: select_abuse_category
identifiers:
- user
product_group: authorization
milestone: '15.9'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167137
distributions:
- ce
tiers:
- free
- premium
- ultimate
additional_properties:
property:
description: The abuse category that was reported

View File

@ -1,8 +0,0 @@
---
name: enforce_ssh_certificates_via_settings
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136498
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426235
milestone: '16.7'
type: development
group: group::source code
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: ssh_certificates_rest_endpoints
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130866
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/424501
milestone: '16.4'
type: development
group: group::source code
default_enabled: false

View File

@ -1,9 +1,9 @@
---
name: expand_nested_variables_in_job_rules_exists_and_changes
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327780
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166779
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/483115
milestone: '17.6'
group: group::pipeline authoring
name: api_audit_requests_with_scope
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/499461
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/505974
milestone: '17.7'
group: group::duo workflow
type: gitlab_com_derisk
default_enabled: false

View File

@ -195,6 +195,9 @@ country_access_logs:
column: user_id
on_delete: async_delete
dast_pre_scan_verifications:
- table: ci_pipelines
column: ci_pipeline_id
on_delete: async_delete
- table: p_ci_pipelines
column: ci_pipeline_id
on_delete: async_delete
@ -213,6 +216,9 @@ dast_profiles:
column: project_id
on_delete: async_delete
dast_profiles_pipelines:
- table: ci_pipelines
column: ci_pipeline_id
on_delete: async_delete
- table: p_ci_pipelines
column: ci_pipeline_id
on_delete: async_delete
@ -266,6 +272,9 @@ dependency_list_export_parts:
column: organization_id
on_delete: async_delete
dependency_list_exports:
- table: ci_pipelines
column: pipeline_id
on_delete: async_delete
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_delete
@ -323,6 +332,9 @@ members:
column: user_id
on_delete: async_delete
merge_request_metrics:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_nullify
@ -337,10 +349,16 @@ merge_request_requested_changes:
column: user_id
on_delete: async_delete
merge_requests:
- table: ci_pipelines
column: head_pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: head_pipeline_id
on_delete: async_nullify
merge_trains:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_nullify
@ -434,6 +452,9 @@ p_ci_stages:
column: project_id
on_delete: async_delete
packages_build_infos:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_nullify
@ -444,6 +465,9 @@ packages_npm_metadata_caches:
target_column: status
target_value: 2
packages_package_file_build_infos:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_nullify
@ -508,6 +532,9 @@ sbom_components:
column: organization_id
on_delete: async_delete
sbom_occurrences:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_nullify
@ -610,6 +637,9 @@ vulnerability_external_issue_links:
column: author_id
on_delete: async_delete
vulnerability_feedback:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: pipeline_id
on_delete: async_nullify
@ -671,6 +701,12 @@ vulnerability_namespace_historical_statistics:
column: namespace_id
on_delete: async_delete
vulnerability_occurrences:
- table: ci_pipelines
column: initial_pipeline_id
on_delete: async_nullify
- table: ci_pipelines
column: latest_pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: initial_pipeline_id
on_delete: async_nullify
@ -707,6 +743,9 @@ vulnerability_state_transitions:
column: author_id
on_delete: async_nullify
vulnerability_statistics:
- table: ci_pipelines
column: latest_pipeline_id
on_delete: async_nullify
- table: p_ci_pipelines
column: latest_pipeline_id
on_delete: async_nullify

View File

@ -0,0 +1,8 @@
---
migration_job_name: BackfillDetectedAtFromCreatedAtColumn
description: backfills the vulnerabilities table with nil detected_at with values from created_at.
feature_category: vulnerability_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170022
milestone: '17.6'
queued_migration_version: 20241021063020
finalized_by: # version of the migration that finalized this BBM

View File

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

View File

@ -0,0 +1 @@
6031eb946f78909f5934e1476f6fc4458cde1a900488b9b9d9e290d3fdeab659

View File

@ -12,9 +12,7 @@ DETAILS:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.4 [with a flag](../user/feature_flags.md) named `ssh_certificates_rest_endpoints`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 16.9.
FLAG:
On GitLab.com, this feature is available. On GitLab Dedicated, this feature is not available.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 17.7. Feature flag `ssh_certificates_rest_endpoints` removed.
Use this API to create, read and delete SSH certificates for a group.
Only top-level groups can store SSH certificates.

View File

@ -261,6 +261,12 @@ Audit event types belong to the following product categories.
| [`cluster_agent_token_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Triggered when a user creates a cluster agent token | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project |
| [`cluster_agent_token_revoked`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/112036) | Triggered when a user revokes a cluster agent token | **{check-circle}** Yes | GitLab [15.10](https://gitlab.com/gitlab-org/gitlab/-/issues/382133) | Project |
### Duo workflow
| Name | Description | Saved to database | Streamed | Introduced in | Scope |
|:------------|:------------|:------------------|:---------|:--------------|:--------------|
| [`api_request_access_with_scope`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548) | A susbset of API requests authenticated by a token with an audited scope | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/499461) | User |
### Dynamic application security testing
| Name | Description | Saved to database | Introduced in | Scope |

View File

@ -233,6 +233,10 @@ If you encounter issues:
1. Search for the setting **GitLab: Debug** and enable it.
1. Examine the [Duo Workflow Service production LangSmith trace](https://smith.langchain.com/o/477de7ad-583e-47b6-a1c4-c4a0300e7aca/projects/p/5409132b-2cf3-4df8-9f14-70204f90ed9b?timeModel=%7B%22duration%22%3A%227d%22%7D&tab=0).
## Audit log
Audit event is created for each API request done by Duo Workflow. View these events on the [instance audit events](../../administration/audit_event_reports.md#instance-audit-events) page.
## Give feedback
Duo Workflow is an experiment and your feedback is crucial. To report issues or suggest improvements,

View File

@ -21,10 +21,7 @@ To temporarily grant Git access to your projects, you can use SSH certificates.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.4 [with a flag](../feature_flags.md) named `ssh_certificates_rest_endpoints`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 16.9.
FLAG:
The availability of this feature is controlled by a feature flag.
For more information, see the history.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/424501) in GitLab 17.7. Feature flag `ssh_certificates_rest_endpoints` removed.
Prerequisites:
@ -69,6 +66,7 @@ The user certificates can only be used to access the projects in the top-level g
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.7 [with a flag](../feature_flags.md) named `enforce_ssh_certificates_via_settings`. Disabled by default.
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/426235) in GitLab 16.9.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/488635) in GitLab 17.7. Feature flag `enforce_ssh_certificates_via_settings` removed.
FLAG:
The availability of this feature is controlled by a feature flag.

View File

@ -19,6 +19,10 @@ module API
API_RESPONSE_STATUS_CODE = 'gitlab.api.response_status_code'
INTEGER_ID_REGEX = /^-?\d+$/
# ai_workflows scope is used by Duo Workflow which is an AI automation tool, requests authenticated by token with
# this scope are audited to keep track of all actions done by Duo Workflow.
TOKEN_SCOPES_TO_AUDIT = [:ai_workflows].freeze
def logger
API.logger
end
@ -89,6 +93,7 @@ module API
if @current_user
load_balancer_stick_request(::ApplicationRecord, :user, @current_user.id)
audit_request_with_token_scope(@current_user)
end
@current_user
@ -816,6 +821,29 @@ module API
end
end
def audit_request_with_token_scope(user)
return unless Feature.enabled?(:api_audit_requests_with_scope, user)
token_info = request.env[::Gitlab::Auth::AuthFinders::API_TOKEN_ENV]
return unless token_info
return unless TOKEN_SCOPES_TO_AUDIT.intersect?(Array.wrap(token_info[:token_scopes]))
context = {
name: 'api_request_access_with_scope',
author: user,
scope: user,
target: ::Gitlab::Audit::NullTarget.new,
message: "API request with token scopes #{token_info[:token_scopes]} - #{request.request_method} #{request.path}",
additional_details: {
request: request.path,
method: request.request_method,
token_scopes: token_info[:token_scopes]
}
}
::Gitlab::Audit::Auditor.audit(context)
end
private
# rubocop:disable Gitlab/ModuleWithInstanceVariables

View File

@ -45,28 +45,31 @@ module BulkImports
return unless object
if object.new_record?
saver = Gitlab::ImportExport::Base::RelationObjectSaver.new(
relation_object: object,
relation_key: relation,
relation_definition: relation_definition,
importable: portable
)
begin
if object.new_record?
saver = Gitlab::ImportExport::Base::RelationObjectSaver.new(
relation_object: object,
relation_key: relation,
relation_definition: relation_definition,
importable: portable
)
saver.execute
saver.execute
capture_invalid_subrelations(saver.invalid_subrelations)
else
if object.invalid?
Gitlab::Import::Errors.merge_nested_errors(object)
capture_invalid_subrelations(saver.invalid_subrelations)
else
if object.invalid?
Gitlab::Import::Errors.merge_nested_errors(object)
raise(ActiveRecord::RecordInvalid, object)
raise(ActiveRecord::RecordInvalid, object)
end
object.save!
end
object.save!
ensure
push_placeholder_references(original_users_map) if context.importer_user_mapping_enabled?
end
push_placeholder_references(original_users_map) if context.importer_user_mapping_enabled?
end
def deep_transform_relation!(relation_hash, relation_key, relation_definition, &block)

View File

@ -221,7 +221,11 @@ module Gitlab
private
def save_current_token_in_env
request.env[API_TOKEN_ENV] = { token_id: access_token.id, token_type: access_token.class.to_s }
request.env[API_TOKEN_ENV] = {
token_id: access_token.id,
token_type: access_token.class.to_s,
token_scopes: access_token.scopes.map(&:to_sym)
}
end
def save_auth_failure_in_application_context(access_token, cause, requested_scopes)

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class BackfillDetectedAtFromCreatedAtColumn < BatchedMigrationJob
operation_name :backfill_vulnerabilities_detected_at
feature_category :vulnerability_management
def perform
each_sub_batch do |sub_batch|
sub_batch.where(detected_at: nil).update_all('detected_at = created_at')
end
end
end
end
end

View File

@ -17,7 +17,7 @@ module Gitlab
return true unless modified_paths
return false if modified_paths.empty?
expanded_globs = expand_globs(context, pipeline).uniq
expanded_globs = expand_globs(context).uniq
return false if expanded_globs.empty?
cache_key = [
@ -43,17 +43,11 @@ module Gitlab
end
end
def expand_globs(context, pipeline)
def expand_globs(context)
return paths unless context
if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
paths.map do |glob|
expand_value_nested(glob, context)
end
else
paths.map do |glob|
expand_value(glob, context)
end
paths.map do |glob|
expand_value_nested(glob, context)
end
end
@ -76,22 +70,13 @@ module Gitlab
def find_compare_to_sha(pipeline, context)
return unless @globs.include?(:compare_to)
compare_to = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
expand_value_nested(@globs[:compare_to], context)
else
expand_value(@globs[:compare_to], context)
end
compare_to = expand_value_nested(@globs[:compare_to], context)
commit = pipeline.project.commit(compare_to)
raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit
commit.sha
end
def expand_value(value, context)
ExpandVariables.expand_existing(value, -> { context.variables_hash })
end
def expand_value_nested(value, context)
ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded })
end

View File

@ -18,13 +18,13 @@ module Gitlab
@ref = clause[:ref]
end
def satisfied_by?(pipeline, context)
def satisfied_by?(_pipeline, context)
# Return early to avoid redundant Gitaly calls
return false unless @globs.any?
context = change_context(context, pipeline) if @project_path
context = change_context(context) if @project_path
expanded_globs = expand_globs(context, pipeline)
expanded_globs = expand_globs(context)
top_level_only = expanded_globs.all?(&method(:top_level_glob?))
paths = worktree_paths(context, top_level_only)
@ -42,15 +42,9 @@ module Gitlab
grouped.values_at(:exact, :extension, :pattern).map { |globs| Array(globs) }
end
def expand_globs(context, pipeline)
if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline&.project)
@globs.map do |glob|
expand_value_nested(glob, context)
end
else
@globs.map do |glob|
expand_value(glob, context)
end
def expand_globs(context)
@globs.map do |glob|
expand_value_nested(glob, context)
end
end
@ -127,10 +121,10 @@ module Gitlab
glob.delete_prefix(WILDCARD_NESTED_PATTERN)
end
def change_context(old_context, pipeline)
def change_context(old_context)
user = find_context_user(old_context)
new_project = find_context_project(user, old_context, pipeline)
new_sha = find_context_sha(new_project, old_context, pipeline)
new_project = find_context_project(user, old_context)
new_sha = find_context_sha(new_project, old_context)
Gitlab::Ci::Config::External::Context.new(
project: new_project,
@ -144,13 +138,8 @@ module Gitlab
context.is_a?(Gitlab::Ci::Config::External::Context) ? context.user : context.pipeline.user
end
def find_context_project(user, context, pipeline)
full_path = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
expand_value_nested(@project_path, context)
else
expand_value(@project_path, context)
end
def find_context_project(user, context)
full_path = expand_value_nested(@project_path, context)
project = Project.find_by_full_path(full_path)
unless project
@ -167,16 +156,10 @@ module Gitlab
project
end
def find_context_sha(project, context, pipeline)
def find_context_sha(project, context)
return project.commit&.sha unless @ref
ref = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes,
pipeline.project)
expand_value_nested(@ref, context)
else
expand_value(@ref, context)
end
ref = expand_value_nested(@ref, context)
commit = project.commit(ref)
unless commit
@ -198,10 +181,6 @@ module Gitlab
end
end
def expand_value(value, context)
ExpandVariables.expand_existing(value, -> { context.variables_hash })
end
def expand_value_nested(value, context)
ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded })
end

View File

@ -295,6 +295,16 @@ module Gitlab
def being_watched_cache_key
"gitlab:ci:trace:#{job.id}:watched"
end
# Like `set` it writes the whole trace but doesn't hide secrets!
# This is solely used in spec factories to avoid calling unstubbed
# ApplicationSetting(ci_job_token_signing_key) attribute outside
# spec examples (like in `let_it_be`).
def unsafe_set(data)
write('w+b') do |stream|
stream.set(data.dup)
end
end
end
end
end

View File

@ -163,7 +163,6 @@ module Gitlab
{ service: 'gitaly.RepositoryService', method: 'RepositorySize' },
{ service: 'gitaly.RepositoryService', method: 'SearchFilesByContent' },
{ service: 'gitaly.RepositoryService', method: 'SearchFilesByName' },
{ service: 'gitaly.ServerService', method: 'ClockSynced' },
{ service: 'gitaly.ServerService', method: 'DiskStatistics' },
{ service: 'gitaly.ServerService', method: 'ReadinessCheck' },
{ service: 'gitaly.ServerService', method: 'ServerInfo' },

View File

@ -33,7 +33,7 @@ namespace :tw do
CodeOwnerRule.new('Code Review', '@aqualls'),
CodeOwnerRule.new('Compliance', '@eread'),
CodeOwnerRule.new('Composition Analysis', '@rdickenson @phillipwells'),
CodeOwnerRule.new('Container Registry', '@marcel.amirault'),
CodeOwnerRule.new('Container Registry', '@lyspin'),
CodeOwnerRule.new('Contributor Experience', '@eread'),
CodeOwnerRule.new('Custom Models', '@jglassman1'),
# CodeOwnerRule.new('Database', ''),

View File

@ -19307,6 +19307,9 @@ msgstr ""
msgid "DesignManagement|Are you sure you want to cancel creating this comment?"
msgstr ""
msgid "DesignManagement|Are you sure you want to cancel editing the description?"
msgstr ""
msgid "DesignManagement|Are you sure you want to cancel editing this comment?"
msgstr ""

View File

@ -262,7 +262,12 @@ FactoryBot.define do
trait :trace_live do
after(:create) do |build, evaluator|
Gitlab::ExclusiveLease.skipping_transaction_check { build.trace.set('BUILD TRACE') }
Gitlab::ExclusiveLease.skipping_transaction_check do
# We can skip calling `Ci::Build#hide_secrets` because this content is safe.
# This allows not to call into potentialy unstubbed ApplicationSetting in specs.
# For example: `ci_job_token_signing_key` when in `let_it_be` context.
build.trace.send(:unsafe_set, 'BUILD TRACE')
end
end
end

View File

@ -11,7 +11,7 @@ RSpec.describe 'Upload a nuget package', :api, :js, feature_category: :package_r
let(:api_path) { "/projects/#{project.id}/packages/nuget/" }
let(:url) { capybara_url(api(api_path)) }
let(:file) { File.open(expand_fixture_path('packages/nuget/package.nupkg')) }
let(:file) { fixture_file_upload('spec/fixtures/packages/nuget/package.nupkg') }
subject do
HTTParty.put(

View File

@ -12,6 +12,7 @@ import ModelVersionActionsDropdown from '~/ml/model_registry/components/model_ve
import { createAlert } from '~/alert';
import {
modelVersionWithCandidateAndAuthor,
modelVersionWithCandidateAndNullAuthor,
deleteModelVersionResponses,
} from '../graphql_mock_data';
@ -36,6 +37,7 @@ describe('ModelVersionsTable', () => {
deleteResolver = jest.fn().mockResolvedValue(deleteModelVersionResponses.success),
canWriteModelRegistry = true,
mountFn = mountExtended,
tableItems = items,
} = {}) => {
const requestHandlers = [[deleteModelVersionMutation, deleteResolver]];
apolloProvider = createMockApollo(requestHandlers);
@ -45,7 +47,7 @@ describe('ModelVersionsTable', () => {
canWriteModelRegistry,
},
propsData: {
items,
items: tableItems,
canWriteModelRegistry,
},
apolloProvider,
@ -108,6 +110,12 @@ describe('ModelVersionsTable', () => {
expect(avatarLink.text()).toContain(items[0].author.name);
});
it('renders the author information correctly for items with no author', () => {
createWrapper({ tableItems: [modelVersionWithCandidateAndNullAuthor] });
const avatarLink = findTableRows().at(0).findComponent(GlAvatarLink);
expect(avatarLink.exists()).toBe(false);
});
it('renders actions dropdown if canWriteModelRegistry is true', () => {
createWrapper({}, true);
expect(findActionsDropdown().exists()).toBe(true);

View File

@ -146,6 +146,11 @@ export const modelVersionWithCandidateAndAuthor = {
},
};
export const modelVersionWithCandidateAndNullAuthor = {
...modelVersionWithCandidateAndAuthor,
author: null,
};
export const graphqlCandidates = [
{
id: 'gid://gitlab/Ml::Candidate/1',

View File

@ -299,26 +299,6 @@ describe('Packages Settings', () => {
});
});
describe('with packagesAllowDuplicateExceptions FF enabled', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
mountComponent({
mountFn: mountExtended,
features: { packagesAllowDuplicateExceptions: true },
});
const { nugetDuplicateExceptionRegex } = packageSettings;
expect(findNugetDuplicatedSettingsExceptionsInput().props()).toMatchObject({
duplicatesAllowed: false,
duplicateExceptionRegex: nugetDuplicateExceptionRegex,
duplicateExceptionRegexError: '',
loading: false,
name: 'nugetDuplicateExceptionRegex',
id: 'nuget-duplicated-settings-regex-input',
});
});
});
it('on update event calls the mutation', () => {
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
mountComponent({ mountFn: mountExtended, mutationResolver });

View File

@ -1,25 +1,291 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import updateDesignDescriptionMutation from '~/work_items/components/design_management/graphql/update_design_description.mutation.graphql';
import DesignDescription from '~/work_items/components/design_management/design_preview/design_description.vue';
import mockDesign from './mock_design';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { mockTracking } from 'helpers/tracking_helper';
import { UPDATE_DESCRIPTION_ERROR } from '~/work_items/components/design_management/constants';
import { mockUpdateDesignDescriptionResponse, designDescriptionFactory } from '../mock_data';
jest.mock('~/behaviors/markdown/render_gfm');
Vue.use(VueApollo);
describe('DesignDescription', () => {
let wrapper;
function createComponent() {
const mockDesignVariables = {
fullPath: '',
iid: '1',
filenames: ['test.jpg'],
atVersion: null,
};
const formFieldProps = {
id: 'design-description',
name: 'design-description',
placeholder: 'Write a comment or drag your files here…',
'aria-label': 'Design description',
};
const updatedDescription = 'New test description';
const mockDesign = designDescriptionFactory();
const findDesignContent = () => wrapper.findByTestId('design-description-content');
const findEditDescriptionButton = () => wrapper.findByTestId('edit-description');
const findSaveDescriptionButton = () => wrapper.findByTestId('save-description');
const findCancelDescriptionButton = () => wrapper.findByTestId('cancel');
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
const findCheckboxAtIndex = (index) => wrapper.findAll('input[type="checkbox"]').at(index);
const findAlert = () => wrapper.findComponent(GlAlert);
const findAddDesignDescriptionButton = () => wrapper.findByTestId('add-design-description');
const updateDescriptionMutationSuccessHandler = jest
.fn()
.mockResolvedValue(mockUpdateDesignDescriptionResponse);
const mockDesignUpdateResponseHandler = jest.fn().mockResolvedValue(
mockUpdateDesignDescriptionResponse({
description: updatedDescription,
descriptionHtml: `<p data-sourcepos="1:1-1:16" dir="auto">${updatedDescription}</p>`,
}),
);
const updateDescriptionMutationError = jest
.fn()
.mockRejectedValue(new Error(UPDATE_DESCRIPTION_ERROR));
const createComponent = ({
design = mockDesign,
descriptionText = '',
isSubmitting = false,
designUpdateMutationHandler = updateDescriptionMutationSuccessHandler,
} = {}) => {
wrapper = shallowMountExtended(DesignDescription, {
apolloProvider: createMockApollo([
[updateDesignDescriptionMutation, designUpdateMutationHandler],
]),
propsData: {
design: mockDesign,
design,
designVariables: mockDesignVariables,
markdownPreviewPath: '/gitlab-org/gitlab-test/preview_markdown?target_type=Issue',
},
data() {
return {
formFieldProps,
descriptionText,
isSubmitting,
};
},
});
}
};
beforeEach(() => {
createComponent();
});
it('renders design description', () => {
expect(wrapper.findByTestId('design-description-content').text()).toBe(
mockDesign.descriptionHtml,
);
expect(wrapper.findByTestId('design-description-content').text()).toBe(mockDesign.description);
});
describe('user has no updateDesign permission', () => {
it('renders description content without edit button', () => {
createComponent({
design: designDescriptionFactory({
updateDesign: false,
}),
});
expect(findDesignContent().text()).toEqual('Description test');
expect(findEditDescriptionButton().exists()).toBe(false);
});
});
describe('user has updateDesign permission', () => {
it('renders description content with the edit button', () => {
createComponent();
expect(findDesignContent().text()).toEqual('Description test');
expect(findEditDescriptionButton().exists()).toBe(true);
});
it('renders save button when editor is open', async () => {
createComponent();
await findEditDescriptionButton().vm.$emit('click');
expect(findSaveDescriptionButton().exists()).toBe(true);
expect(findSaveDescriptionButton().attributes('disabled')).toBeUndefined();
});
it('renders add a description button when there is no description', () => {
createComponent({
design: designDescriptionFactory({
description: '',
descriptionHtml: '',
}),
});
expect(findMarkdownEditor().exists()).toBe(false);
expect(findAddDesignDescriptionButton().exists()).toBe(true);
});
it('renders description form when add a description button is clicked', async () => {
createComponent({
design: designDescriptionFactory({
description: '',
descriptionHtml: '',
}),
});
expect(findAddDesignDescriptionButton().exists()).toBe(true);
expect(findMarkdownEditor().exists()).toBe(false);
await findAddDesignDescriptionButton().vm.$emit('click');
expect(findMarkdownEditor().exists()).toBe(true);
expect(findAddDesignDescriptionButton().exists()).toBe(false);
});
it('resets description text if empty when form is closed', async () => {
createComponent();
await findEditDescriptionButton().vm.$emit('click');
findMarkdownEditor().vm.$emit('input', '');
await findCancelDescriptionButton().vm.$emit('click');
expect(findDesignContent().text()).toEqual('Description test');
});
it('triggers mutation when form is submitted and hides the form', async () => {
const trackingSpy = mockTracking(undefined, null, jest.spyOn);
createComponent({
designUpdateMutationHandler: mockDesignUpdateResponseHandler,
});
await findEditDescriptionButton().vm.$emit('click');
findMarkdownEditor().vm.$emit('input', updatedDescription);
findSaveDescriptionButton().vm.$emit('click');
await nextTick();
expect(mockDesignUpdateResponseHandler).toHaveBeenCalledWith({
input: {
description: updatedDescription,
id: 'gid:/gitlab/Design/1',
},
});
await waitForPromises();
expect(findMarkdownEditor().exists()).toBe(false);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'save_markdown', {
label: 'markdown_editor',
property: 'Design',
});
});
it('shows error message when mutation fails', async () => {
createComponent({
descriptionText: updatedDescription,
designUpdateMutationHandler: updateDescriptionMutationError,
});
await findEditDescriptionButton().vm.$emit('click');
findMarkdownEditor().vm.$emit('input', updatedDescription);
findSaveDescriptionButton().vm.$emit('click');
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(UPDATE_DESCRIPTION_ERROR);
});
describe('content has checkboxes', () => {
const mockCheckboxDescription = '- [x] todo 1\n- [ ] todo 2';
const mockCheckboxDescriptionHtml = `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
<li class="task-list-item" data-sourcepos="1:1-2:15">
<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li>
<li class="task-list-item" data-sourcepos="2:1-2:15">
<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
</ul>`;
const checkboxDesignDescription = designDescriptionFactory({
updateDesign: true,
description: mockCheckboxDescription,
descriptionHtml: mockCheckboxDescriptionHtml,
});
const mockCheckedDescriptionUpdateResponseHandler = jest.fn().mockResolvedValue(
mockUpdateDesignDescriptionResponse({
description: '- [x] todo 1\n- [x] todo 2',
descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
<li class="task-list-item" data-sourcepos="1:1-2:15">
<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li>
<li class="task-list-item" data-sourcepos="2:1-2:15">
<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
</ul>`,
}),
);
const mockUnCheckedDescriptionUpdateResponseHandler = jest.fn().mockResolvedValue(
mockUpdateDesignDescriptionResponse({
description: '- [ ] todo 1\n- [ ] todo 2',
descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
<li class="task-list-item" data-sourcepos="1:1-2:15">
<input class="task-list-item-checkbox" type="checkbox"> todo 1</li>
<li class="task-list-item" data-sourcepos="2:1-2:15">
<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
</ul>`,
}),
);
it.each`
assertionName | mockUpdateCheckboxesResponseHandler | checkboxIndex | checked | expectedDesignDescription
${'checked'} | ${mockCheckedDescriptionUpdateResponseHandler} | ${1} | ${true} | ${'- [x] todo 1\n- [x] todo 2'}
${'unchecked'} | ${mockUnCheckedDescriptionUpdateResponseHandler} | ${0} | ${false} | ${'- [ ] todo 1\n- [ ] todo 2'}
`(
'updates the store object when checkbox is $assertionName',
async ({
mockUpdateCheckboxesResponseHandler,
checkboxIndex,
checked,
expectedDesignDescription,
}) => {
createComponent({
design: checkboxDesignDescription,
descriptionText: mockCheckboxDescription,
designUpdateMutationHandler: mockUpdateCheckboxesResponseHandler,
});
findCheckboxAtIndex(checkboxIndex).setChecked(checked);
expect(mockUpdateCheckboxesResponseHandler).toHaveBeenCalledWith({
input: {
description: expectedDesignDescription,
id: 'gid:/gitlab/Design/1',
},
});
await waitForPromises();
expect(renderGFM).toHaveBeenCalled();
},
);
it('disables checkbox while updating', () => {
createComponent({
design: checkboxDesignDescription,
descriptionText: mockCheckboxDescription,
});
findCheckboxAtIndex(1).setChecked();
expect(findCheckboxAtIndex(1).attributes().disabled).toBeDefined();
});
});
});
});

View File

@ -40,6 +40,13 @@ describe('DesignSidebar', () => {
},
});
const mockDesignVariables = {
fullPath: 'gitlab-org/gitlab-shell',
iid: '1',
filenames: ['image_name.png'],
atVersion: null,
};
function createComponent({ design = mockDesign, isLoading = false, isLoggedIn = true } = {}) {
if (isLoggedIn) {
window.gon.current_user_id = 1;
@ -49,8 +56,10 @@ describe('DesignSidebar', () => {
propsData: {
design,
isLoading,
designVariables: mockDesignVariables,
isOpen: true,
resolvedDiscussionsExpanded: false,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
},
mocks: {
$route,

View File

@ -16,6 +16,32 @@ export const mockDesign = {
nodes: [],
__typename: 'DiscussionConnection',
},
issue: {
id: 'gid:/gitlab/Issue/1',
title: 'My precious issue',
webPath: 'full-issue-path',
webUrl: 'full-issue-url',
participants: {
nodes: [
{
name: 'Administrator',
id: 'gid://gitlab/User/1"',
username: 'root',
webUrl: 'link-to-author',
webPath: '/root',
avatarUrl: 'link-to-avatar',
__typename: 'UserCore',
},
],
__typename: 'UserCoreConnection',
},
userPermissions: {
createDesign: true,
updateDesign: true,
__typename: 'IssuePermissions',
},
__typename: 'Issue',
},
__typename: 'Design',
};
@ -84,6 +110,53 @@ export const designCollectionResponse = (mockDesigns = [mockDesign]) => ({
},
});
export const designDescriptionFactory = ({
updateDesign = true,
description = 'Description test',
descriptionHtml = '<p data-sourcepos="1:1-1:16" dir="auto">Description test</p>',
} = {}) => ({
id: 'gid:/gitlab/Design/1',
iid: 1,
filename: 'test.jpg',
fullPath: 'full-design-path',
image: 'test.jpg',
description,
descriptionHtml,
updatedAt: '01-01-2019',
updatedBy: {
name: 'test',
},
issue: {
id: 'gid:/gitlab/Issue/1',
title: 'My precious issue',
webPath: 'full-issue-path',
webUrl: 'full-issue-url',
participants: {
nodes: [
{
name: 'Administrator',
username: 'root',
webUrl: 'link-to-author',
avatarUrl: 'link-to-avatar',
__typename: 'UserCore',
},
],
__typename: 'UserCoreConnection',
},
userPermissions: {
updateDesign,
awardEmoji: true,
__typename: 'IssuePermissions',
},
__typename: 'Issue',
},
discussions: {
nodes: [],
__typename: 'DiscussionConnection',
},
__typename: 'Design',
});
export const allDesignsArchivedResponse = () => ({
data: {
workItem: {
@ -134,6 +207,18 @@ export const getDesignResponse = {
},
};
export const mockUpdateDesignDescriptionResponse = (options) => {
return {
data: {
designManagementUpdate: {
errors: [],
design: designDescriptionFactory(options),
},
__typename: 'DesignManagementUpdatePayload',
},
};
};
export const mockArchiveDesignMutationResponse = {
data: {
designManagementDelete: {

View File

@ -458,6 +458,61 @@ RSpec.describe BulkImports::NdjsonPipeline, feature_category: :importers do
subject.load(nil, [merge_request, original_users_map])
end
end
context 'when an exception is raised when saving a nested object' do
it 'still pushes a placeholder reference for the persisted objects' do
merge_request = build(:merge_request,
source_project: project,
target_project: project,
author: source_user_1.mapped_user
)
note = build(:note,
project: project,
author: source_user_1.mapped_user
)
diff_note = build(:diff_note_on_merge_request,
project: project,
author: source_user_1.mapped_user
)
allow(diff_note).to receive(:save).and_raise(DiffNote::NoteDiffFileCreationError)
merge_request.notes << note
merge_request.notes << diff_note
original_users_map = {}.compare_by_identity
original_users_map[merge_request] = {
'author_id' => source_user_1.source_user_identifier
}
original_users_map[note] = {
'author_id' => source_user_1.source_user_identifier
}
original_users_map[diff_note] = {
'author_id' => source_user_1.source_user_identifier
}
expect(Import::PlaceholderReferences::PushService).to receive(:from_record).with(
import_source: ::Import::SOURCE_DIRECT_TRANSFER,
import_uid: context.bulk_import_id,
record: merge_request,
user_reference_column: :author_id,
source_user: source_user_1
).and_call_original
expect(Import::PlaceholderReferences::PushService).to receive(:from_record).with(
import_source: ::Import::SOURCE_DIRECT_TRANSFER,
import_uid: context.bulk_import_id,
record: note,
user_reference_column: :author_id,
source_user: source_user_1
).and_call_original
expect { subject.load(nil, [merge_request, original_users_map]) }.to raise_error(
DiffNote::NoteDiffFileCreationError
)
end
end
end
end

View File

@ -17,7 +17,6 @@ RSpec.describe ::Ci::JobToken::Jwt::Decode, feature_category: :continuous_integr
end
before do
stub_feature_flags(ci_job_token_jwt: true)
allow(::Ci::JobToken::Jwt::Encode).to receive(:key).and_return(key)
allow(described_class).to receive(:key).and_return(key)
end

View File

@ -1043,10 +1043,6 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching, feature_cate
let_it_be(:user) { create(:user) }
before do
stub_feature_flags(ci_job_token_jwt: true)
end
context 'for running build' do
let!(:build) { create(:ci_build, :running, user: user) }

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillDetectedAtFromCreatedAtColumn, :migration, schema: 20241021063020, feature_category: :vulnerability_management do
let(:migration) do
described_class.new(
start_id: vulnerability_without_detected_at.id,
end_id: vulnerability_without_detected_at.id,
batch_table: :vulnerabilities,
batch_column: batch_column,
sub_batch_size: sub_batch_size,
pause_ms: pause_ms,
connection: ApplicationRecord.connection
)
end
let(:batch_column) { :id }
let(:sub_batch_size) { 1_000 }
let(:pause_ms) { 0 }
let(:projects_table) { table(:projects) }
let(:namespaces_table) { table(:namespaces) }
let(:vulnerabilities_table) { table(:vulnerabilities) }
let(:vulnerability_identifiers_table) { table(:vulnerability_identifiers) }
let(:vulnerability_occurrences_table) { table(:vulnerability_occurrences) }
let(:scanners_table) { table(:vulnerability_scanners) }
let(:namespace) { namespaces_table.create!(name: 'test', path: 'test') }
let(:project) do
projects_table.create!(
namespace_id: namespace.id,
name: 'test',
path: 'test',
project_namespace_id: namespace.id
)
end
# Add a scanner (required for vulnerability_occurrences)
let(:scanner) do
scanners_table.create!(
project_id: project.id,
external_id: 'test_scanner',
name: 'test_scanner'
)
end
# Create vulnerability identifier (required for vulnerability_occurrences)
let(:identifier) do
vulnerability_identifiers_table.create!(
project_id: project.id,
fingerprint: 'test-identifier',
external_type: 'test_type',
external_id: 'test_id',
name: 'test_name'
)
end
# Create vulnerability occurrences (findings)
let!(:finding1) do
vulnerability_occurrences_table.create!(
project_id: project.id,
scanner_id: scanner.id,
primary_identifier_id: identifier.id,
project_fingerprint: SecureRandom.hex(20),
location_fingerprint: SecureRandom.hex(20),
name: 'Test finding 1',
severity: 5,
confidence: 5,
report_type: 1,
uuid: SecureRandom.uuid,
created_at: 2.days.ago,
metadata_version: 'something',
raw_metadata: {}
)
end
let!(:finding2) do
vulnerability_occurrences_table.create!(
project_id: project.id,
scanner_id: scanner.id,
primary_identifier_id: identifier.id,
project_fingerprint: SecureRandom.hex(20),
location_fingerprint: SecureRandom.hex(20),
name: 'Test finding 2',
severity: 5,
confidence: 5,
report_type: 1,
uuid: SecureRandom.uuid,
created_at: 4.days.ago,
metadata_version: 'something',
raw_metadata: {}
)
end
let!(:vulnerability_without_detected_at) do
vulnerabilities_table.create!(
project_id: project.id,
author_id: 1,
title: 'Test vulnerability 1',
severity: 5,
confidence: 5,
report_type: 1,
state: 1,
finding_id: finding1.id,
created_at: 2.days.ago
)
end
let!(:vulnerability_with_detected_at) do
vulnerabilities_table.create!(
project_id: project.id,
author_id: 1,
title: 'Test vulnerability 2',
severity: 5,
confidence: 5,
report_type: 1,
state: 1,
finding_id: finding2.id,
created_at: 4.days.ago,
detected_at: 3.days.ago
)
end
before do
# detected_at default value is NOW(), so update it to NULL
vulnerability_without_detected_at.update_column :detected_at, nil
end
describe '#perform' do
it 'backfills detected_at with created_at for vulnerabilities with nil detected_at' do
expect(vulnerabilities_table.where(detected_at: nil).count).to eq 1
migration.perform
expect(vulnerabilities_table.where(detected_at: nil).count).to eq 0
end
it 'does not modify vulnerabilities that already have detected_at' do
expect { migration.perform }.not_to change {
vulnerabilities_table.find(vulnerability_with_detected_at.id).detected_at
}
end
end
end

View File

@ -177,15 +177,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
end
it { is_expected.to be_truthy }
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
allow(context).to receive(:variables_hash).and_return(variables_hash)
end
it { is_expected.to be_falsey }
end
end
end
@ -293,15 +284,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
it { is_expected.to be_truthy }
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
allow(context).to receive(:variables_hash).and_return(variables_hash)
end
it { is_expected.to be_truthy }
end
context 'when the variable is nested' do
let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) }
let(:variables_hash) do
@ -322,19 +304,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
end
it { is_expected.to be_truthy }
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
allow(context).to receive(:variables_hash).and_return(variables_hash)
end
it 'raises ParseError' do
expect { satisfied_by }.to raise_error(
::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref'
)
end
end
end
end
end

View File

@ -72,14 +72,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:globs) { ['$NESTED_FULL_PATH_VALID'] }
it { is_expected.to be_truthy }
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it { is_expected.to be_falsey }
end
end
end
@ -130,14 +122,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:globs) { ['$FILE_TXT'] }
it { is_expected.to be_truthy }
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it { is_expected.to be_truthy }
end
end
context 'when the project path is invalid' do
@ -159,19 +143,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
"rules:exists:project `invalid/path/subdir` is not a valid project path"
)
end
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it 'raises an error' do
expect { satisfied_by? }.to raise_error(
Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
"rules:exists:project `invalid/path/subdir` is not a valid project path"
)
end
end
end
context 'when the project path contains a masked variable' do
@ -202,14 +173,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:ref) { '$NEW_BRANCH' }
it { is_expected.to be_truthy }
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it { is_expected.to be_truthy }
end
end
context 'when the ref is invalid' do
@ -232,20 +195,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
"in project `#{other_project.full_path}`"
)
end
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it 'raises an error' do
expect { satisfied_by? }.to raise_error(
Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
"rules:exists:ref `invalid/ref/new_branch` is not a valid ref " \
"in project `#{other_project.full_path}`"
)
end
end
end
context 'when the ref contains a masked variable' do

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillDetectedAtFromCreatedAtColumn, migration: :gitlab_sec, feature_category: :vulnerability_management 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(
gitlab_schema: :gitlab_sec,
table_name: :vulnerabilities,
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

@ -6125,7 +6125,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
let(:token) { 'my-token' }
before do
stub_feature_flags(ci_job_token_jwt: true)
allow_next_instance_of(::Ci::JobToken::Jwt::Encode, build) do |encode|
allow(encode).to receive(:jwt).and_return(token)
end

View File

@ -18,6 +18,7 @@ RSpec.describe UserDetail, feature_category: :system_access do
let(:registration_type) { 'free' }
let(:glm_source) { 'glm_source' }
let(:glm_content) { 'glm_content' }
let(:joining_project) { true }
let(:onboarding_status) do
{
step_url: step_url,
@ -25,7 +26,8 @@ RSpec.describe UserDetail, feature_category: :system_access do
initial_registration_type: registration_type,
registration_type: registration_type,
glm_source: glm_source,
glm_content: glm_content
glm_content: glm_content,
joining_project: joining_project
}
end
@ -127,6 +129,22 @@ RSpec.describe UserDetail, feature_category: :system_access do
end
end
context 'for joining_project' do
let(:onboarding_status) do
{
joining_project: joining_project
}
end
it { is_expected.to allow_value(onboarding_status).for(:onboarding_status) }
context "when 'joining_project' is invalid" do
let(:joining_project) { 'true' }
it { is_expected.not_to allow_value(onboarding_status).for(:onboarding_status) }
end
end
context 'when there is no data' do
let(:onboarding_status) { {} }

View File

@ -516,4 +516,79 @@ RSpec.describe API::API, feature_category: :system_access do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'audit logging of requests with a specific token scope' do
let_it_be(:user) { create(:user) }
let_it_be(:token) { create(:oauth_access_token, user: user, scopes: [:ai_workflows]) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:path) { "/projects/#{issue.project.id}/issues/#{issue.iid}" }
before_all do
project.add_developer(user)
end
shared_examples 'audited request' do
it 'adds audit log' do
expect(::Gitlab::Audit::Auditor).to receive(:audit).with(hash_including({
name: 'api_request_access_with_scope',
message: "API request with token scopes [:ai_workflows] - GET /api/v4#{path}"
})).and_call_original
subject
expect(response).to have_gitlab_http_status(status)
end
end
shared_examples 'not audited request' do
it "doesn't add audit log" do
expect(::Gitlab::Audit::Auditor).not_to receive(:audit)
subject
expect(response).to have_gitlab_http_status(status)
end
end
context 'when endpoint allows token with ai_workflow scope' do
subject { get api(path, oauth_access_token: token) }
context 'when token with ai_workflows scope is used' do
let(:status) { :ok }
it_behaves_like 'audited request'
context 'when request fails' do
let_it_be(:path) { "/projects/#{issue.project.id}/issues/#{non_existing_record_id}" }
let(:status) { :not_found }
it_behaves_like 'audited request'
end
context 'when api_audit_requests_with_scope flag is disabled' do
before do
stub_feature_flags(api_audit_requests_with_scope: false)
end
it_behaves_like 'not audited request'
end
end
context 'when token with ai_workflows scope is not used' do
let_it_be(:token) { create(:oauth_access_token, user: user, scopes: [:api]) }
let(:status) { :ok }
it_behaves_like 'not audited request'
end
end
context "when endpoint doesn't allow token with ai_workflow scope" do
subject { delete api(path, oauth_access_token: token) }
let(:status) { :forbidden }
it_behaves_like 'not audited request'
end
end
end

View File

@ -83,6 +83,23 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
end
end
shared_examples 'includes statistics when all_available is false' do
let(:params) { { all_available: false, statistics: true } }
before do
group1.add_developer(admin)
create(:group, parent: group1)
end
it 'returns the statistics', :aggregate_failures do
get api(api_endpoint, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first).to include('statistics')
end
end
describe "GET /groups" do
shared_examples 'groups list N+1' do
it 'avoids N+1 queries', :use_sql_query_cache do
@ -351,6 +368,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
expect(json_response[0]["statistics"].keys).to match_array(stat_keys)
end
it_behaves_like 'includes statistics when all_available is false' do
let(:api_endpoint) { "/groups" }
end
end
context "when using skip_groups in request" do
@ -2507,6 +2528,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
expect(json_response).to be_an Array
expect(json_response.first).to include('statistics')
end
it_behaves_like 'includes statistics when all_available is false' do
let(:api_endpoint) { "/groups/#{group1.id}/subgroups" }
end
end
it_behaves_like 'skips searching in full path' do
@ -2646,6 +2671,10 @@ RSpec.describe API::Groups, :with_current_organization, feature_category: :group
expect(json_response).to be_an Array
expect(json_response.first).to include('statistics')
end
it_behaves_like 'includes statistics when all_available is false' do
let(:api_endpoint) { "/groups/#{group1.id}/descendant_groups" }
end
end
it_behaves_like 'skips searching in full path' do

View File

@ -249,17 +249,6 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('job1', 'job2', 'job4')
end
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it 'creates all relevant jobs' do
expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('job1', 'job2')
end
end
end
end
@ -906,16 +895,6 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
it 'creates both jobs' do
expect(build_names).to contain_exactly('job1', 'job2')
end
context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
before do
stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
end
it 'does not create job1' do
expect(build_names).to contain_exactly('job2')
end
end
end
end
end

View File

@ -6,7 +6,6 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:file_name) { 'package.nupkg' }
let_it_be_with_reload(:package_settings) { create(:namespace_package_setting, :group, namespace: project.namespace) }
let(:params) do
{
@ -44,57 +43,13 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
context 'with nuget_duplicate_exception_regex' do
before do
package_settings.update!(nuget_duplicate_exception_regex: ".*#{existing_package.name.last(3)}.*")
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
end
it_behaves_like 'returning success'
end
end
shared_examples 'when package file is in object storage' do
let(:params) { super().merge(remote_url: 'https://example.com') }
before do
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
allow(instance).to receive(:execute)
.and_return(ServiceResponse.success(payload: Nokogiri::XML::Document.new))
end
allow_next_instance_of(::Packages::Nuget::ExtractMetadataContentService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
end
end
it_behaves_like 'handling duplicates disallowed when package exists'
context 'when ExtractRemoteMetadataFileService raises ExtractionError' do
before do
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
allow(instance).to receive(:execute).and_raise(
::Packages::Nuget::ExtractRemoteMetadataFileService::ExtractionError, 'nuspec file not found'
)
end
end
it_behaves_like 'returning error', reason: :bad_request, message: 'nuspec file not found'
end
context 'when version is normalized' do
let(:metadata) { super().merge(package_version: '1.7.15') }
it_behaves_like 'handling duplicates disallowed when package exists'
end
end
shared_examples 'when package file is on disk' do
before do
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
end
end
it_behaves_like 'handling duplicates disallowed when package exists'
end
context 'with existing package' do
let_it_be(:existing_package) { create(:nuget_package, :with_metadatum, project: project, version: '1.7.15.0') }
let_it_be(:metadata) do
@ -106,67 +61,63 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
}
end
before do
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
end
end
context 'when nuget duplicates are allowed' do
before do
package_settings.update!(nuget_duplicates_allowed: true)
allow_next_instance_of(Namespace::PackageSetting) do |instance|
allow(instance).to receive(:nuget_duplicates_allowed?).and_return(true)
end
end
it_behaves_like 'returning success'
context 'when the package name matches the exception regex' do
before do
package_settings.update!(nuget_duplicate_exception_regex: existing_package.name)
end
it_behaves_like 'returning error', reason: :conflict,
message: 'A package with the same name and version already exists'
end
context 'when the package version matches the exception regex' do
before do
package_settings.update!(nuget_duplicate_exception_regex: existing_package.version)
end
it_behaves_like 'returning error', reason: :conflict,
message: 'A package with the same name and version already exists'
end
end
context 'when nuget duplicates are not allowed' do
before do
package_settings.update!(nuget_duplicates_allowed: false)
let!(:package_settings) do
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
end
it_behaves_like 'when package file is in object storage'
it_behaves_like 'when package file is on disk'
end
context 'when package file is in object storage' do
let(:params) { super().merge(remote_url: 'https://example.com') }
context 'when packages_allow_duplicate_exceptions is disabled' do
before do
stub_feature_flags(packages_allow_duplicate_exceptions: false)
end
context 'when nuget duplicates are allowed' do
before do
package_settings.update!(nuget_duplicates_allowed: true)
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
allow(instance).to receive(:execute)
.and_return(ServiceResponse.success(payload: Nokogiri::XML::Document.new))
end
allow_next_instance_of(::Packages::Nuget::ExtractMetadataContentService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
end
end
it_behaves_like 'returning success'
end
it_behaves_like 'handling duplicates disallowed when package exists'
context 'when nuget duplicates are not allowed' do
before do
package_settings.update!(nuget_duplicates_allowed: false)
context 'when ExtractRemoteMetadataFileService raises ExtractionError' do
before do
allow_next_instance_of(::Packages::Nuget::ExtractRemoteMetadataFileService) do |instance|
allow(instance).to receive(:execute).and_raise(
::Packages::Nuget::ExtractRemoteMetadataFileService::ExtractionError, 'nuspec file not found'
)
end
end
it_behaves_like 'returning error', reason: :bad_request, message: 'nuspec file not found'
end
it_behaves_like 'when package file is in object storage'
it_behaves_like 'when package file is on disk'
context 'when version is normalized' do
let(:metadata) { super().merge(package_version: '1.7.15') }
it_behaves_like 'handling duplicates disallowed when package exists'
end
end
context 'when package file is on disk' do
before do
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
end
end
it_behaves_like 'handling duplicates disallowed when package exists'
end
end
end
@ -183,42 +134,20 @@ RSpec.describe Packages::Nuget::CheckDuplicatesService, feature_category: :packa
end
context 'when nuget duplicates are allowed' do
before do
package_settings.nuget_duplicates_allowed = true
let_it_be(:package_settings) do
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: true)
end
it_behaves_like 'returning success'
end
context 'when nuget duplicates are not allowed' do
before do
package_settings.nuget_duplicates_allowed = false
let_it_be(:package_settings) do
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
end
it_behaves_like 'returning success'
end
context 'when packages_allow_duplicate_exceptions is disabled' do
before do
stub_feature_flags(packages_allow_duplicate_exceptions: false)
end
context 'when nuget duplicates are allowed' do
before do
package_settings.nuget_duplicates_allowed = true
end
it_behaves_like 'returning success'
end
context 'when nuget duplicates are not allowed' do
before do
package_settings.nuget_duplicates_allowed = false
end
it_behaves_like 'returning success'
end
end
end
end
end

View File

@ -356,10 +356,6 @@ RSpec.configure do |config|
# Our test suite is setup to test plain text editor by default with separate tests just
# for the rich text editor. Switch the flag off to continue testing the same way as before
stub_feature_flags(rich_text_editor_as_default: false)
# TODO: temporary measure to unblock master
# issue: https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/9437
stub_feature_flags(ci_job_token_jwt: false)
else
unstub_all_feature_flags
end

View File

@ -652,12 +652,6 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
)
end
before do
allow_next_instance_of(::Packages::Nuget::CheckDuplicatesService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success)
end
end
it { is_expected.to have_request_urgency(:low) }
context 'with valid project' do
@ -764,86 +758,30 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
it_behaves_like 'returning response status', :forbidden
end
context 'when handling duplicates' do
context 'when package duplicates are not allowed' do
let(:params) { { package: temp_file(file_name, content: File.open(expand_fixture_path('packages/nuget/package.nupkg'))) } }
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
let_it_be(:existing_package) { create(:nuget_package, project: project) }
let_it_be(:metadata) { { package_name: existing_package.name, package_version: existing_package.version } }
let_it_be(:package_settings) do
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
end
before do
allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance|
allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata))
end
allow_next_instance_of(::Packages::Nuget::CheckDuplicatesService) do |instance|
allow(instance).to receive(:execute).and_call_original
end
end
context 'when package duplicates are not allowed' do
let_it_be(:package_settings) do
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false)
end
it_behaves_like 'returning response status', :conflict unless symbol_package
it_behaves_like 'returning response status', :created if symbol_package
it_behaves_like 'returning response status', :conflict unless symbol_package
it_behaves_like 'returning response status', :created if symbol_package
context 'when exception_regex is set' do
before do
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
end
it_behaves_like 'returning response status', :created
end
context 'with packages_allow_duplicate_exceptions disabled' do
before do
stub_feature_flags(packages_allow_duplicate_exceptions: false)
end
it_behaves_like 'returning response status', :conflict unless symbol_package
it_behaves_like 'returning response status', :created if symbol_package
context 'when exception_regex is set' do
before do
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
end
it_behaves_like 'returning response status', :created
end
end
end
context 'when package duplicates are allowed' do
let_it_be(:package_settings) do
create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: true)
context 'when exception_regex is set' do
before do
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
end
it_behaves_like 'returning response status', :created
context 'when exception_regex is set' do
before do
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
end
it_behaves_like 'returning response status', :conflict unless symbol_package
it_behaves_like 'returning response status', :created if symbol_package
end
context 'with packages_allow_duplicate_exceptions disabled' do
before do
stub_feature_flags(packages_allow_duplicate_exceptions: false)
end
it_behaves_like 'returning response status', :created
context 'when exception_regex is set' do
before do
package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*")
end
it_behaves_like 'returning response status', :created
end
end
end
end
end