Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b2bf7e325a
commit
cf83f0a235
|
@ -213,7 +213,7 @@
|
|||
if: '$CI_PROJECT_PATH == "gitlab-org/gitlab-foss" && $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'
|
||||
|
||||
.if-not-dot-com-gitlab-org-and-not-jihulab: &if-not-dot-com-gitlab-org-and-not-jihulab
|
||||
if: '($CI_SERVER_HOST != "gitlab.com" || $CI_PROJECT_ROOT_NAMESPACE != "gitlab-org") && ($CI_SERVER_HOST != "jihulab.com" || $CI_PROJECT_NAMESPACE != "gitlab-cn")'
|
||||
if: '($CI_SERVER_HOST != "gitlab.com" || $CI_PROJECT_NAMESPACE != "gitlab-org") && ($CI_SERVER_HOST != "jihulab.com" || $CI_PROJECT_NAMESPACE != "gitlab-cn")'
|
||||
|
||||
.if-dot-com-gitlab-org-schedule: &if-dot-com-gitlab-org-schedule
|
||||
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_PIPELINE_SOURCE == "schedule"'
|
||||
|
|
61
CHANGELOG.md
61
CHANGELOG.md
|
@ -2,6 +2,30 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 18.1.2 (2025-07-09)
|
||||
|
||||
### Fixed (5 changes)
|
||||
|
||||
- [Rake Doctor Secrets: Fix WebHook error](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ce02068cccff230ffbef2f88b169902fe7f43bbf)
|
||||
- [Fix title on empty projects](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3ea74609f662c78433afcfe160a028a5bbbdf2fc)
|
||||
- [Show both author and committer in last commit](https://gitlab.com/gitlab-org/security/gitlab/-/commit/52ba3c0f90dd0ebc4f6a27beab60588f091068af)
|
||||
- [Remove Sidekiq shutdown delay in ConcurrencyLimitSampler](https://gitlab.com/gitlab-org/security/gitlab/-/commit/03315bd4f35d87ff58220bf581158698ce163b72)
|
||||
- [Fix code owner validation for roles](https://gitlab.com/gitlab-org/security/gitlab/-/commit/e797849679b80d660a34b65f11dd7506e9fdf35b) **GitLab Enterprise Edition**
|
||||
|
||||
### Changed (2 changes)
|
||||
|
||||
- [Fix the owner for sequence ci_builds_id_seq](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d594b6dc14fc5b2ed52f49e7d97d1a2363397185)
|
||||
- [Enable using glab for CI release](https://gitlab.com/gitlab-org/security/gitlab/-/commit/b91e1226900cbdbb1dfd53efd65c9cb2b6d2f64a)
|
||||
|
||||
### Security (6 changes)
|
||||
|
||||
- [Revert "Merge branch..." from 18.1](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5384ab91a8eaaa1cfe253eb093277f76cde48d09) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5126))
|
||||
- [Enforces invite_group_members permission when creating group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/e3f78357e039d70c0eaf67d86f46cced28c8ce3b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5105))
|
||||
- [Enforces invite_project_members permission when creating project members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/064d8e2a0ce7a9c0191c9ec3ef7f43d1f25e8f29) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5108))
|
||||
- [Fix XSS via blob rich viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2cd8baa02ea37d89d2f7c67749947da520cb4ea1) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5119))
|
||||
- [Fix CI ID Token claims for forked project MR jobs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/1a79ece45035eec1d5daee10f89363be089ff069) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5114))
|
||||
- [Prevent linking fork if target group disallows external forks](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3ccce42e662ce3849c8dde62975e21146d6ef0fa) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5102))
|
||||
|
||||
## 18.1.1 (2025-06-24)
|
||||
|
||||
### Security (5 changes)
|
||||
|
@ -922,6 +946,28 @@ entry.
|
|||
- [Change users_preferences.organization_groups_projects_display defaults](https://gitlab.com/gitlab-org/gitlab/-/commit/c0bed48fc7a755413edf1090c86a33a798771d37) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/190331))
|
||||
- [Quarantine a flaky test](https://gitlab.com/gitlab-org/gitlab/-/commit/06fdc6c5fb9a7490c5fe8e6b1eb3a8b0f065f950) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189248))
|
||||
|
||||
## 18.0.4 (2025-07-09)
|
||||
|
||||
### Fixed (8 changes)
|
||||
|
||||
- [Fix incorrect redirect when branch doesn't include files](https://gitlab.com/gitlab-org/security/gitlab/-/commit/3e7fb0bdef7ebc8ac321646a94305eacfd93acc0)
|
||||
- [Fix title on empty projects](https://gitlab.com/gitlab-org/security/gitlab/-/commit/573d6691721b83db8122876d77397212646b251a)
|
||||
- [Show both author and committer in last commit](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5c73962ffc8aab22c863062a62efc269b6dbc996)
|
||||
- [Backport "Add a spinner for a loading elipsis menu" to 18.0](https://gitlab.com/gitlab-org/security/gitlab/-/commit/c90dda26bce7b97e65c92308308b8cd77d7c7c73)
|
||||
- [Refactor blob commit info section](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ee9fbe3f711dfc7b2b51c492e8f24de2253ed698)
|
||||
- [Remove Sidekiq shutdown delay in ConcurrencyLimitSampler](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5c7648701f92856c839eb8d8dbf760fda8c2eac4)
|
||||
- [Fix code owner validation for roles](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2512b4869c9ba658e1c35246843c42aec2ddf555) **GitLab Enterprise Edition**
|
||||
- [Fix Protected Tags show page](https://gitlab.com/gitlab-org/security/gitlab/-/commit/aca613193dbda73c149411055c1bf46fae3447b6)
|
||||
|
||||
### Security (6 changes)
|
||||
|
||||
- [Revert "Merge branch..." from 18.0](https://gitlab.com/gitlab-org/security/gitlab/-/commit/d6168858300ceeac41e4c824198e6a92146a205c) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5127))
|
||||
- [Enforces invite_group_members permission when creating group members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/1f301202958e3cc830ffa5682ae2f852de69a11b) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5106))
|
||||
- [Enforces invite_project_members permission when creating project members](https://gitlab.com/gitlab-org/security/gitlab/-/commit/cf62ff2ceaafae0229005adc818a0a094458e128) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5109))
|
||||
- [Fix XSS via blob rich viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/2638ec4db071db9862fad4e7d46d43cf9363d9c4) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5120))
|
||||
- [Fix CI ID Token claims for forked project MR jobs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/37d0e88ffaa631795f9ef1a37294f9b2a4ff7e36) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5116))
|
||||
- [Prevent linking fork if target group disallows external forks](https://gitlab.com/gitlab-org/security/gitlab/-/commit/48d6c2e6c4022e134d3074ed3de36788ce18175e) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5103))
|
||||
|
||||
## 18.0.3 (2025-06-24)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
@ -1816,6 +1862,21 @@ entry.
|
|||
- [Finalize migration BackfillContainerRepositoryStatesProjectId](https://gitlab.com/gitlab-org/gitlab/-/commit/78f333c76a39d0a85938318b3be49905c19074e6) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/185869))
|
||||
- [Finalize migration BackfillPackagesRpmMetadataProjectId](https://gitlab.com/gitlab-org/gitlab/-/commit/d066d88be1fff7cfcf64017124af797e085a4b4f) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184553))
|
||||
|
||||
## 17.11.6 (2025-07-09)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
||||
- [Fix incorrect redirect when branch doesn't include files](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5261940b88db1ba0078f8d5a68f8d553022e5cb7)
|
||||
- [Fix incompatible Rails cache version from 7.1 to 6.1](https://gitlab.com/gitlab-org/security/gitlab/-/commit/91a9adeec53343019e505416607f9c4606a26aec)
|
||||
- [Fix code owner validation for roles](https://gitlab.com/gitlab-org/security/gitlab/-/commit/b5760803cdee7196c74726887b3fbad541af6a3a) **GitLab Enterprise Edition**
|
||||
|
||||
### Security (4 changes)
|
||||
|
||||
- [Revert "Merge branch..." from 17.11](https://gitlab.com/gitlab-org/security/gitlab/-/commit/5f7dded039c6a95d0cad4e80950730e6600ae096) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5128))
|
||||
- [Fix XSS via blob rich viewer](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ad8aefc5d97748a36211e673de10d4ea3c3528d7) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5121))
|
||||
- [Fix CI ID Token claims for forked project MR jobs](https://gitlab.com/gitlab-org/security/gitlab/-/commit/ed3b2358908fdf6a6cad1bab226a5d08de1ce926) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5117))
|
||||
- [Prevent linking fork if target group disallows external forks](https://gitlab.com/gitlab-org/security/gitlab/-/commit/8d2fe458b23e72778561d1dbb31d13fae68224f4) ([merge request](https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/5104))
|
||||
|
||||
## 17.11.5 (2025-06-24)
|
||||
|
||||
### Changed (2 changes)
|
||||
|
|
|
@ -1 +1 @@
|
|||
14.42.0
|
||||
14.43.0
|
||||
|
|
|
@ -549,7 +549,7 @@
|
|||
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
|
||||
{"name":"racc","version":"1.8.1","platform":"java","checksum":"54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98"},
|
||||
{"name":"racc","version":"1.8.1","platform":"ruby","checksum":"4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f"},
|
||||
{"name":"rack","version":"2.2.13","platform":"ruby","checksum":"ccee101719696a5da12ee9da6fb3b1d20cb329939e089e0e458be6e93667f0fb"},
|
||||
{"name":"rack","version":"2.2.17","platform":"ruby","checksum":"5fe02a1ca80d6fb2271dba00985ee2962d6f5620b6f46dfed89f5301ac4699dd"},
|
||||
{"name":"rack-accept","version":"0.4.5","platform":"ruby","checksum":"66247b5449db64ebb93ae2ec4af4764b87d1ae8a7463c7c68893ac13fa8d4da2"},
|
||||
{"name":"rack-attack","version":"6.7.0","platform":"ruby","checksum":"3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c"},
|
||||
{"name":"rack-cors","version":"2.0.2","platform":"ruby","checksum":"415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b"},
|
||||
|
|
|
@ -1517,7 +1517,7 @@ GEM
|
|||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.13)
|
||||
rack (2.2.17)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.7.0)
|
||||
|
|
|
@ -549,7 +549,7 @@
|
|||
{"name":"raabro","version":"1.4.0","platform":"ruby","checksum":"d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882"},
|
||||
{"name":"racc","version":"1.8.1","platform":"java","checksum":"54f2e6d1e1b91c154013277d986f52a90e5ececbe91465d29172e49342732b98"},
|
||||
{"name":"racc","version":"1.8.1","platform":"ruby","checksum":"4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f"},
|
||||
{"name":"rack","version":"2.2.13","platform":"ruby","checksum":"ccee101719696a5da12ee9da6fb3b1d20cb329939e089e0e458be6e93667f0fb"},
|
||||
{"name":"rack","version":"2.2.17","platform":"ruby","checksum":"5fe02a1ca80d6fb2271dba00985ee2962d6f5620b6f46dfed89f5301ac4699dd"},
|
||||
{"name":"rack-accept","version":"0.4.5","platform":"ruby","checksum":"66247b5449db64ebb93ae2ec4af4764b87d1ae8a7463c7c68893ac13fa8d4da2"},
|
||||
{"name":"rack-attack","version":"6.7.0","platform":"ruby","checksum":"3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c"},
|
||||
{"name":"rack-cors","version":"2.0.2","platform":"ruby","checksum":"415d4e1599891760c5dc9ef0349c7fecdf94f7c6a03e75b2e7c2b54b82adda1b"},
|
||||
|
|
|
@ -1511,7 +1511,7 @@ GEM
|
|||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.13)
|
||||
rack (2.2.17)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.7.0)
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
"ContainerProtectionTagRule"
|
||||
],
|
||||
"AiCatalogItem": [
|
||||
"AiCatalogAgent"
|
||||
"AiCatalogAgent",
|
||||
"AiCatalogFlow"
|
||||
],
|
||||
"AiCatalogItemVersion": [
|
||||
"AiCatalogAgentVersion"
|
||||
"AiCatalogAgentVersion",
|
||||
"AiCatalogFlowVersion"
|
||||
],
|
||||
"AlertManagementIntegration": [
|
||||
"AlertManagementHttpIntegration",
|
||||
|
|
|
@ -314,7 +314,9 @@ export default {
|
|||
|
||||
await this.$nextTick();
|
||||
handleLocationHash(); // Ensures that we scroll to the hash when async content is loaded
|
||||
eventHub.$emit('showBlobInteractionZones', this.blobInfo.path);
|
||||
if (type === SIMPLE_BLOB_VIEWER) {
|
||||
eventHub.$emit('showBlobInteractionZones', this.blobInfo.path);
|
||||
}
|
||||
})
|
||||
.catch(() => this.displayError())
|
||||
.finally(() => {
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
<script>
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import FileBrowserHeight from '~/diffs/components/file_browser_height.vue';
|
||||
import TreeList from './components/tree_list.vue';
|
||||
|
||||
export const TREE_WIDTH = 320;
|
||||
export const MIN_TREE_WIDTH = 240;
|
||||
export const FILE_TREE_BROWSER_STORAGE_KEY = 'file_tree_browser_storage_key';
|
||||
|
||||
export default {
|
||||
name: 'FileTreeBrowser',
|
||||
components: {
|
||||
TreeList,
|
||||
FileBrowserHeight,
|
||||
PanelResizer,
|
||||
},
|
||||
props: {
|
||||
projectPath: {
|
||||
|
@ -35,20 +39,44 @@ export default {
|
|||
return this.$route.name === 'projectRoot';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.restoreTreeWidthUserPreference();
|
||||
},
|
||||
methods: {
|
||||
restoreTreeWidthUserPreference() {
|
||||
const userPreference = localStorage.getItem(FILE_TREE_BROWSER_STORAGE_KEY);
|
||||
if (!userPreference) return;
|
||||
this.treeWidth = parseInt(userPreference, 10);
|
||||
},
|
||||
onSizeUpdate(value) {
|
||||
this.treeWidth = value;
|
||||
},
|
||||
saveTreeWidthPreference(size) {
|
||||
localStorage.setItem(FILE_TREE_BROWSER_STORAGE_KEY, size);
|
||||
this.treeWidth = size;
|
||||
},
|
||||
},
|
||||
fileTreeBrowserStorageKey: FILE_TREE_BROWSER_STORAGE_KEY,
|
||||
minTreeWidth: MIN_TREE_WIDTH,
|
||||
maxTreeWidth: 500,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<file-browser-height
|
||||
v-if="!isProjectOverview"
|
||||
:style="{ width: `${treeWidth}px` }"
|
||||
class="repository-tree-list gl-mt-5"
|
||||
:style="{ '--tree-width': `${treeWidth}px` }"
|
||||
class="repository-tree-list repository-tree-list-responsive gl-mt-5 gl-px-5"
|
||||
>
|
||||
<tree-list
|
||||
class="gl-mr-5"
|
||||
:project-path="projectPath"
|
||||
:current-ref="currentRef"
|
||||
:ref-type="refType"
|
||||
<panel-resizer
|
||||
class="max-lg:gl-hidden"
|
||||
:start-size="treeWidth"
|
||||
:min-size="$options.minTreeWidth"
|
||||
:max-size="$options.maxTreeWidth"
|
||||
side="right"
|
||||
@update:size="onSizeUpdate"
|
||||
@resize-end="saveTreeWidthPreference"
|
||||
/>
|
||||
<tree-list :project-path="projectPath" :current-ref="currentRef" :ref-type="refType" />
|
||||
</file-browser-height>
|
||||
</template>
|
||||
|
|
|
@ -34,7 +34,7 @@ export const FAILURE_REASONS = {
|
|||
merge_request_blocked: __('Merge request dependencies must be merged.'),
|
||||
status_checks_must_pass: __('Status checks must pass.'),
|
||||
jira_association_missing: __('Either the title or description must reference a Jira issue.'),
|
||||
requested_changes: __('The change requests must be completed or resolved.'),
|
||||
requested_changes: __('Change requests must be approved by the requesting user.'),
|
||||
approvals_syncing: __('The merge request approvals are currently syncing.'),
|
||||
locked_paths: __('All paths must be unlocked'),
|
||||
locked_lfs_files: __('All LFS files must be unlocked.'),
|
||||
|
|
|
@ -6,6 +6,7 @@ import DeleteNoteMutation from '~/wikis/graphql/notes/delete_wiki_page_note.muta
|
|||
import { clearDraft, getDraft } from '~/lib/utils/autosave';
|
||||
import { __ } from '~/locale';
|
||||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
|
||||
import { getLocationHash } from '~/lib/utils/url_utility';
|
||||
import { TYPENAME_USER } from '~/graphql_shared/constants';
|
||||
import { createAlert } from '~/alert';
|
||||
import AwardsList from '~/vue_shared/components/awards_list.vue';
|
||||
|
@ -101,18 +102,24 @@ export default {
|
|||
noteAnchorId() {
|
||||
return `note_${this.noteId}`;
|
||||
},
|
||||
isTarget() {
|
||||
return getLocationHash() === this.noteAnchorId;
|
||||
},
|
||||
canAwardEmoji() {
|
||||
return this.note.userPermissions?.awardEmoji;
|
||||
},
|
||||
dynamicClasses() {
|
||||
return {
|
||||
timeLineEntryItem: {
|
||||
'note note-wrapper note-comment': true,
|
||||
[`note-row-${this.noteId}`]: true,
|
||||
'gl-opacity-5 gl-pointer-events-none': this.isUpdating || this.isDeleting,
|
||||
'is-editable': this.canEdit,
|
||||
'internal-note': this.note.internal,
|
||||
target: this.isTarget,
|
||||
},
|
||||
noteParent: {
|
||||
'timeline-content': true,
|
||||
'gl-rounded-lg gl-border gl-border-section': !this.replyNote,
|
||||
'gl-ml-7': this.replyNote,
|
||||
'gl-bg-section gl-ml-8': !this.replyNote,
|
||||
|
@ -255,7 +262,6 @@ export default {
|
|||
:id="noteAnchorId"
|
||||
:class="dynamicClasses.timeLineEntryItem"
|
||||
:data-note-id="noteId"
|
||||
class="note note-wrapper note-comment"
|
||||
data-testid="noteable-note-container"
|
||||
>
|
||||
<div class="timeline-avatar gl-float-left">
|
||||
|
|
|
@ -26,6 +26,14 @@ $bottom-padding: $gl-spacing-scale-6;
|
|||
}
|
||||
}
|
||||
|
||||
.repository-tree-list-responsive {
|
||||
width: var(--tree-width);
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-holder {
|
||||
.nav-block {
|
||||
margin: $gl-spacing-scale-2 0 $gl-spacing-scale-5;
|
||||
|
|
|
@ -23,7 +23,7 @@ module Mutations
|
|||
experiment: { milestone: '18.2' }
|
||||
argument :ids, [::Types::GlobalIDType[::WorkItem]],
|
||||
required: true,
|
||||
description: 'Global ID array of the issues that will be updated. ' \
|
||||
description: 'Global ID array of the work items that will be updated. ' \
|
||||
"IDs that the user can\'t update will be ignored. A max of #{MAX_WORK_ITEMS} can be provided."
|
||||
argument :milestone_widget,
|
||||
::Types::WorkItems::Widgets::MilestoneInputType,
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module WorkItems
|
||||
class WidgetsResolver < BaseResolver
|
||||
type [::GraphQL::Types::String], null: true
|
||||
|
||||
MAX_TYPES = 100
|
||||
|
||||
argument :ids, [::Types::GlobalIDType[::WorkItems::Type]],
|
||||
required: true,
|
||||
description: <<~DESC.squish
|
||||
Global ID array of work items types to fetch available widgets for.
|
||||
A max of #{MAX_TYPES} IDs can be provided at a time.
|
||||
DESC
|
||||
|
||||
def ready?(**args)
|
||||
if args[:ids].size > MAX_TYPES
|
||||
raise Gitlab::Graphql::Errors::ArgumentError,
|
||||
format(
|
||||
_('No more than %{max_work_items} work items can be loaded at the same time'),
|
||||
max_work_items: MAX_TYPES
|
||||
)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def resolve(ids:)
|
||||
::WorkItems::Type
|
||||
.id_in(ids.map(&:model_id))
|
||||
.reduce(Set.new) do |result, type|
|
||||
types = type.widgets(resource_parent).map { |widget| widget.widget_type.upcase }
|
||||
result.union(types)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_parent
|
||||
object.respond_to?(:sync) ? object.sync : object
|
||||
end
|
||||
strong_memoize_attr :resource_parent
|
||||
end
|
||||
end
|
||||
end
|
|
@ -113,6 +113,12 @@ module Types
|
|||
experiment: { milestone: '18.1' },
|
||||
resolver: ::Resolvers::Namespaces::WorkItemsResolver
|
||||
|
||||
field :work_items_widgets,
|
||||
null: true,
|
||||
description: 'List of available widgets for the given work items.',
|
||||
experiment: { milestone: '18.2' },
|
||||
resolver: ::Resolvers::WorkItems::WidgetsResolver
|
||||
|
||||
field :work_item_types, Types::WorkItems::TypeType.connection_type,
|
||||
resolver: Resolvers::WorkItems::TypesResolver,
|
||||
experiment: { milestone: '17.2' },
|
||||
|
|
|
@ -71,4 +71,4 @@ class ResourceStateEvent < ResourceEvent
|
|||
end
|
||||
end
|
||||
|
||||
ResourceStateEvent.prepend_mod_with('ResourceStateEvent')
|
||||
ResourceStateEvent.prepend_mod
|
||||
|
|
|
@ -46,3 +46,5 @@ module Members
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Members::Projects::CreatorService.prepend_mod_with('Members::Projects::CreatorService')
|
||||
|
|
|
@ -17,8 +17,8 @@ module WebHooks
|
|||
end
|
||||
|
||||
def execute
|
||||
update_hook_failure_state
|
||||
log_execution
|
||||
update_hook_failure_state
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -63,19 +63,20 @@ module WebHooks
|
|||
hook.parent.update_last_webhook_failure(hook) if hook.parent
|
||||
end
|
||||
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
|
||||
raise if raise_lock_error?
|
||||
# In case the lock is not obtained due to numerous concurrent requests,
|
||||
# we do not attempt to update the hook status.
|
||||
#
|
||||
# This should be fine as if the lock is not obtained, it is likely due
|
||||
# to many concurrent job executions, and eventually one of these should
|
||||
# successfully obtain the lease and update the hook status.
|
||||
rescue StandardError => e
|
||||
# To avoid WebHookLog being created twice in case an exception is raised
|
||||
# when updating the hook status and the job retried.
|
||||
Gitlab::ErrorTracking.track_exception(e, hook_id: hook.id)
|
||||
end
|
||||
|
||||
def lock_name
|
||||
"web_hooks:update_hook_failure_state:#{hook.id}"
|
||||
end
|
||||
|
||||
# Allow an error to be raised after failing to obtain a lease only if the hook
|
||||
# is not already in the correct failure state.
|
||||
def raise_lock_error?
|
||||
hook.reset # Reload so properties are guaranteed to be current.
|
||||
|
||||
hook.executable? != (response_category == :ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
- if repository_file_tree_browser_enabled
|
||||
.gl-flex.navigation-root
|
||||
#js-file-browser
|
||||
.gl-w-full.gl-min-w-0
|
||||
#tree-holder.tree-holder.clearfix.js-per-page.gl-mt-5{ data: { blame_per_page: Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE } }
|
||||
= render 'projects/tree_content', project: project, ref: ref, pipeline: pipeline, tree: @tree, ref_type: @ref_type
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
- if repository_file_tree_browser_enabled
|
||||
.gl-flex.navigation-root
|
||||
#js-file-browser
|
||||
#tree-holder.tree-holder.gl-pt-4.gl-w-full.gl-min-w-0
|
||||
#tree-holder.tree-holder.gl-pt-4.gl-pl-4.gl-w-full.gl-min-w-0
|
||||
= render 'blob', blob: @blob
|
||||
- else
|
||||
#tree-holder.tree-holder.gl-pt-4
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
- if repository_file_tree_browser_enabled
|
||||
.gl-flex.navigation-root
|
||||
#js-file-browser
|
||||
.gl-w-full.gl-min-w-0
|
||||
.gl-w-full.gl-min-w-0.gl-pl-4
|
||||
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
|
||||
- else
|
||||
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
name: container_registry_immutable_tags
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/515996
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183135
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/505455
|
||||
milestone: '17.11'
|
||||
group: group::container registry
|
||||
type: beta
|
||||
default_enabled: false
|
|
@ -81,7 +81,7 @@ of whether you can use these features with Duo Core or Duo Pro when
|
|||
| [Refactor Code](../../user/gitlab_duo_chat/examples.md#refactor-code-in-the-ide) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
|
||||
| [Fix Code](../../user/gitlab_duo_chat/examples.md#fix-code-in-the-ide) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.9 and later | Generally available |
|
||||
| [Root Cause Analysis](../../user/gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | {{< icon name="check-circle-filled" >}} Yes | GitLab 17.10 and later | Beta |
|
||||
| [Vulnerability Explanation](../../user/application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
|
||||
| [Vulnerability Explanation](../../user/application_security/vulnerabilities/_index.md#vulnerability-explanation) | {{< icon name="check-circle-filled" >}} Yes | GitLab 18.1 and later | Beta |
|
||||
|
||||
For more examples of a question you can ask, see
|
||||
[Ask about GitLab](../../user/gitlab_duo_chat/examples.md).
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
---
|
||||
stage: AI-powered
|
||||
group: Custom Models
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
description: Troubleshooting tips for GitLab Duo Self-Hosted
|
||||
title: GitLab Duo Self-Hosted Support Engineer Playbook
|
||||
---
|
||||
|
||||
{{< details >}}
|
||||
|
||||
- Tier: Premium, Ultimate
|
||||
- Add-on: GitLab Duo Enterprise
|
||||
- Offering: GitLab Self-Managed
|
||||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/12972) in GitLab 17.1 [with a flag](../feature_flags/_index.md) named `ai_custom_model`. Disabled by default.
|
||||
- [Enabled on GitLab Self-Managed](https://gitlab.com/groups/gitlab-org/-/epics/15176) in GitLab 17.6.
|
||||
- Changed to require GitLab Duo add-on in GitLab 17.6 and later.
|
||||
- Feature flag `ai_custom_model` removed in GitLab 17.8.
|
||||
- Generally available in GitLab 17.9.
|
||||
- Changed to include Premium in GitLab 18.0.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
## Support Engineer Playbook and Common Issues
|
||||
|
||||
This section provides Support Engineers with essential commands and troubleshooting steps for debugging GitLab Duo Self-Hosted issues.
|
||||
|
||||
## Essential Debugging Commands
|
||||
|
||||
### Display AI Gateway Environment Variables
|
||||
|
||||
Check all AI Gateway environment variables to verify configuration:
|
||||
|
||||
```shell
|
||||
docker exec -it <ai-gateway-container> env | grep AIGW
|
||||
```
|
||||
|
||||
Key variables to verify:
|
||||
|
||||
- `AIGW_CUSTOM_MODELS__ENABLED` - must be `true`
|
||||
- `AIGW_GITLAB_URL` - should match your GitLab instance URL
|
||||
- `AIGW_GITLAB_API_URL` - should be accessible from the container
|
||||
- `AIGW_AUTH__BYPASS_EXTERNAL` - should only be `true` during troubleshooting
|
||||
|
||||
### Verify User Permissions
|
||||
|
||||
Check if a user has the correct permissions for Code Suggestions with self-hosted models:
|
||||
|
||||
```ruby
|
||||
# In GitLab Rails console
|
||||
user = User.find_by_id("<user_id>")
|
||||
user.allowed_to_use?(:code_suggestions, service_name: :self_hosted_models)
|
||||
```
|
||||
|
||||
### Examine AI Gateway Client Logs
|
||||
|
||||
View AI Gateway client logs to identify connection issues:
|
||||
|
||||
```shell
|
||||
docker logs <ai-gateway-container> | grep "Gitlab::Llm::AiGateway::Client"
|
||||
```
|
||||
|
||||
### View GitLab Logs for AI Gateway Requests
|
||||
|
||||
To see the actual requests made to the AI Gateway, use:
|
||||
|
||||
```shell
|
||||
# View live logs
|
||||
sudo gitlab-ctl tail | grep -E "(ai_gateway|llm\.log)"
|
||||
|
||||
# View specific log file with JSON formatting
|
||||
sudo cat /var/log/gitlab/gitlab-rails/llm.log | jq '.'
|
||||
|
||||
# Filter for specific request types
|
||||
sudo cat /var/log/gitlab/gitlab-rails/llm.log | jq 'select(.message)'
|
||||
|
||||
sudo cat /var/log/gitlab/gitlab-rails/llm.log | grep Llm::CompletionWorker | jq '.'
|
||||
```
|
||||
|
||||
### View AI Gateway Logs for Model Requests
|
||||
|
||||
To see the actual requests sent to the model:
|
||||
|
||||
```shell
|
||||
# View AI Gateway container logs
|
||||
docker logs <ai-gateway-container> 2>&1 | grep -E "(model|litellm|custom_openai)"
|
||||
|
||||
# For structured logs, if available
|
||||
docker logs <ai-gateway-container> 2>&1 | grep "model_endpoint"
|
||||
```
|
||||
|
||||
## Common Configuration Issues and Solutions
|
||||
|
||||
### Missing `/v1` Suffix in Model Endpoint
|
||||
|
||||
**Symptom**: 404 errors when making requests to vLLM or OpenAI-compatible models
|
||||
|
||||
**How to spot in logs**:
|
||||
|
||||
```shell
|
||||
# Look for 404 errors in AI Gateway logs
|
||||
docker logs <ai-gateway-container> | grep "404"
|
||||
```
|
||||
|
||||
**Solution**: Ensure the model endpoint includes the `/v1` suffix:
|
||||
|
||||
- Incorrect: `http://localhost:4000`
|
||||
- Correct: `http://localhost:4000/v1`
|
||||
|
||||
### Certificate Validation Issues
|
||||
|
||||
**Symptom**: SSL certificate errors or connection failures
|
||||
|
||||
**How to spot in logs**:
|
||||
|
||||
```shell
|
||||
# Look for SSL/TLS errors
|
||||
sudo cat /var/log/gitlab/gitlab-rails/llm.log | grep -i "ssl\|certificate\|tls"
|
||||
```
|
||||
|
||||
**Validation**: Verify certificate status - GitLab server must use a trusted certificate, as self-signed certificates are not supported.
|
||||
|
||||
**Solution**:
|
||||
|
||||
- Use trusted certificates for GitLab instance
|
||||
- If using self-signed certificates, configure proper certificate paths in the AI Gateway container
|
||||
|
||||
### Network Connectivity Issues
|
||||
|
||||
**Symptom**: Timeouts or connection refused errors
|
||||
|
||||
**How to spot in logs**:
|
||||
|
||||
```shell
|
||||
# Look for network-related errors
|
||||
docker logs <ai-gateway-container> | grep -E "(timeout|connection|refused|unreachable)"
|
||||
```
|
||||
|
||||
**Validation commands**:
|
||||
|
||||
```shell
|
||||
# Test from AI Gateway container to GitLab
|
||||
docker exec -it <ai-gateway-container> curl "$AIGW_GITLAB_API_URL/projects"
|
||||
|
||||
# Test from AI Gateway container to model endpoint
|
||||
docker exec -it <ai-gateway-container> curl "<model_endpoint>/health"
|
||||
```
|
||||
|
||||
### Authentication and Authorization Issues
|
||||
|
||||
**Symptom**: 401 Unauthorized or 403 Forbidden errors
|
||||
|
||||
**How to spot in logs**:
|
||||
|
||||
```shell
|
||||
# Look for authentication errors
|
||||
sudo cat /var/log/gitlab/gitlab-rails/llm.log | jq 'select(.status == 401 or .status == 403)'
|
||||
```
|
||||
|
||||
**Common causes**:
|
||||
|
||||
- User doesn't have GitLab Duo Enterprise seat assigned
|
||||
- License issues
|
||||
- Incorrect AI Gateway URL configuration
|
||||
|
||||
### Model Configuration Issues
|
||||
|
||||
**Symptom**: Model not responding or returning errors
|
||||
|
||||
**How to spot in logs**:
|
||||
|
||||
```shell
|
||||
# Look for model-specific errors
|
||||
docker logs <ai-gateway-container> | grep -E "(model_name|model_endpoint|litellm)"
|
||||
```
|
||||
|
||||
**Validation**:
|
||||
|
||||
```shell
|
||||
# Test model directly from AI Gateway container
|
||||
docker exec -it <ai-gateway-container> sh
|
||||
curl --request POST "<model_endpoint>/v1/chat/completions" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"model": "<model_name>", "messages": [{"role": "user", "content": "Hello"}]}'
|
||||
```
|
||||
|
||||
## Log Analysis Workflow
|
||||
|
||||
### Step 1: Enable Verbose Logging
|
||||
|
||||
Check if the `expanded_ai_logging` feature flag is enabled, in GitLab Rails console:
|
||||
|
||||
```ruby
|
||||
Feature.enabled?(:expanded_ai_logging)
|
||||
```
|
||||
|
||||
If it returns `false`, enable the flag using:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:expanded_ai_logging)
|
||||
```
|
||||
|
||||
### Step 2: Reproduce the Issue
|
||||
|
||||
Have the user reproduce the issue while monitoring logs:
|
||||
|
||||
```shell
|
||||
# Terminal 1: Monitor GitLab logs
|
||||
sudo gitlab-ctl tail | grep -E "(ai_gateway|llm\.log)"
|
||||
|
||||
# Terminal 2: Monitor AI Gateway logs
|
||||
docker logs -f <ai-gateway-container>
|
||||
```
|
||||
|
||||
### Step 3: Analyze Request Flow
|
||||
|
||||
1. **GitLab to AI Gateway**: Check if request reaches AI Gateway
|
||||
1. **AI Gateway to Model**: Verify model endpoint is called
|
||||
1. **Response Path**: Ensure response is properly formatted and returned
|
||||
|
||||
### Step 4: Common Error Patterns
|
||||
|
||||
| Error Pattern | Location | Likely Cause |
|
||||
|---------------|----------|--------------|
|
||||
| `Connection refused` | GitLab logs | AI Gateway not accessible |
|
||||
| `404 Not Found` | AI Gateway logs | Missing `/v1` in model endpoint |
|
||||
| `401 Unauthorized` | GitLab logs | Authentication/license issues |
|
||||
| `Timeout` | Either | Network or model performance issues |
|
||||
| `SSL certificate verify failed` | GitLab logs | Certificate validation issues |
|
||||
|
||||
## Quick Diagnostic Commands
|
||||
|
||||
## **AI Gateway Instance Commands:**
|
||||
|
||||
**1. Test AI Gateway health:**
|
||||
|
||||
```shell
|
||||
curl --silent --output /dev/null --write-out "%{http_code}" "<ai-gateway-url>/monitoring/healthz"
|
||||
```
|
||||
|
||||
**2. Check AI Gateway environment variables:**
|
||||
|
||||
```shell
|
||||
docker exec <ai-gateway-container> env | grep AIGW
|
||||
```
|
||||
|
||||
**3. Check AI Gateway logs for errors:**
|
||||
|
||||
```shell
|
||||
docker logs <ai-gateway-container> 2>&1 | grep --ignore-case error | tail --lines=20
|
||||
```
|
||||
|
||||
## **GitLab Self-Managed Instance Commands:**
|
||||
|
||||
**4. Check user permissions (GitLab Rails console):**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails console
|
||||
```
|
||||
|
||||
Then in the console:
|
||||
|
||||
```ruby
|
||||
User.find_by_id('<user_id>').can?(:access_code_suggestions)
|
||||
```
|
||||
|
||||
**5. Check GitLab LLM logs for errors:**
|
||||
|
||||
```shell
|
||||
sudo tail --lines=100 /var/log/gitlab/gitlab-rails/llm.log | grep --ignore-case error
|
||||
```
|
||||
|
||||
**6. Check feature flags:**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails console
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```ruby
|
||||
Feature.enabled?(:expanded_ai_logging)
|
||||
```
|
||||
|
||||
**7. Test connectivity from GitLab to AI Gateway:**
|
||||
|
||||
```shell
|
||||
curl --verbose "<ai-gateway-url>/monitoring/healthz"
|
||||
```
|
||||
|
||||
### Emergency Diagnostic One-liner
|
||||
|
||||
For quick issue identification:
|
||||
|
||||
```shell
|
||||
# Check all critical components at once
|
||||
docker exec <ai-gateway-container> env | grep AIGW_CUSTOM_MODELS__ENABLED && \
|
||||
curl --silent "<ai-gateway-url>/monitoring/healthz" && \
|
||||
sudo tail --lines=10 /var/log/gitlab/gitlab-rails/llm.log | jq '.level'
|
||||
```
|
||||
|
||||
## Escalation Criteria
|
||||
|
||||
Escalate to Custom Models team when:
|
||||
|
||||
1. **All basic troubleshooting steps completed** without resolution
|
||||
1. **Model integration issues** that require deep technical knowledge
|
||||
1. **Feature not listed** in self-hosted models unit primitives
|
||||
1. **Suspected GitLab Duo platform bugs** affecting multiple users
|
||||
1. **Performance issues** with specific model configurations
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [AI Gateway Installation Guide](../../install/install_ai_gateway.md)
|
||||
- [GitLab Duo Self-Hosted Troubleshooting](troubleshooting.md)
|
|
@ -515,3 +515,4 @@ If a feature is not working or a feature button (for example, **`/troubleshoot`*
|
|||
## Related topics
|
||||
|
||||
- [GitLab Duo troubleshooting](../../user/gitlab_duo_chat/troubleshooting.md)
|
||||
- [Support Engineer Playbook and Common Issues](support_engineer_playbook.md)
|
||||
|
|
|
@ -13102,7 +13102,7 @@ Input type: `WorkItemBulkUpdateInput`
|
|||
| <a id="mutationworkitembulkupdateconfidential"></a>`confidential` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
|
||||
| <a id="mutationworkitembulkupdatehealthstatuswidget"></a>`healthStatusWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetHealthStatusInput`](#workitemwidgethealthstatusinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
|
||||
| <a id="mutationworkitembulkupdatehierarchywidget"></a>`hierarchyWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
|
||||
| <a id="mutationworkitembulkupdateids"></a>`ids` | [`[WorkItemID!]!`](#workitemid) | Global ID array of the issues that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
|
||||
| <a id="mutationworkitembulkupdateids"></a>`ids` | [`[WorkItemID!]!`](#workitemid) | Global ID array of the work items that will be updated. IDs that the user can't update will be ignored. A max of 100 can be provided. |
|
||||
| <a id="mutationworkitembulkupdateiterationwidget"></a>`iterationWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetIterationInput`](#workitemwidgetiterationinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
|
||||
| <a id="mutationworkitembulkupdatelabelswidget"></a>`labelsWidget` | [`WorkItemWidgetLabelsUpdateInput`](#workitemwidgetlabelsupdateinput) | Input for labels widget. |
|
||||
| <a id="mutationworkitembulkupdatemilestonewidget"></a>`milestoneWidget` {{< icon name="warning-solid" >}} | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | **Deprecated**: **Status**: Experiment. Introduced in GitLab 18.2. |
|
||||
|
@ -21887,6 +21887,36 @@ An AI catalog agent version.
|
|||
| <a id="aicatalogagentversionuserprompt"></a>`userPrompt` | [`String`](#string) | User prompt for the agent. |
|
||||
| <a id="aicatalogagentversionversionname"></a>`versionName` | [`String`](#string) | Version name of the item version. |
|
||||
|
||||
### `AiCatalogFlow`
|
||||
|
||||
An AI catalog flow.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="aicatalogflowcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
|
||||
| <a id="aicatalogflowdescription"></a>`description` | [`String!`](#string) | Description of the item. |
|
||||
| <a id="aicatalogflowid"></a>`id` | [`ID!`](#id) | ID of the item. |
|
||||
| <a id="aicatalogflowitemtype"></a>`itemType` | [`AiCatalogItemType!`](#aicatalogitemtype) | Type of the item. |
|
||||
| <a id="aicatalogflowname"></a>`name` | [`String!`](#string) | Name of the item. |
|
||||
| <a id="aicatalogflowproject"></a>`project` | [`Project`](#project) | Project for the item. |
|
||||
| <a id="aicatalogflowversions"></a>`versions` | [`AiCatalogItemVersionConnection`](#aicatalogitemversionconnection) | Versions of the item. (see [Connections](#connections)) |
|
||||
|
||||
### `AiCatalogFlowVersion`
|
||||
|
||||
An AI catalog flow version.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="aicatalogflowversioncreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the item version was created. |
|
||||
| <a id="aicatalogflowversionid"></a>`id` | [`ID!`](#id) | ID of the item version. |
|
||||
| <a id="aicatalogflowversionpublishedat"></a>`publishedAt` | [`Time`](#time) | Timestamp of when the item version was published. |
|
||||
| <a id="aicatalogflowversionupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the item version was updated. |
|
||||
| <a id="aicatalogflowversionversionname"></a>`versionName` | [`String`](#string) | Version name of the item version. |
|
||||
|
||||
### `AiConversationsThread`
|
||||
|
||||
Conversation thread of the AI feature.
|
||||
|
@ -30654,6 +30684,23 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="groupworkitemsweight"></a>`weight` | [`String`](#string) | Weight applied to the work item, "none" and "any" values are supported. |
|
||||
| <a id="groupworkitemsweightwildcardid"></a>`weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
|
||||
|
||||
##### `Group.workItemsWidgets`
|
||||
|
||||
List of available widgets for the given work items.
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 18.2.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Returns [`[String!]`](#string).
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="groupworkitemswidgetsids"></a>`ids` | [`[WorkItemsTypeID!]!`](#workitemstypeid) | Global ID array of work items types to fetch available widgets for. A max of 100 IDs can be provided at a time. |
|
||||
|
||||
##### `Group.workspacesClusterAgents`
|
||||
|
||||
Cluster agents in the namespace with workspaces capabilities.
|
||||
|
@ -34954,6 +35001,23 @@ four standard [pagination arguments](#pagination-arguments):
|
|||
| <a id="namespaceworkitemsweight"></a>`weight` | [`String`](#string) | Weight applied to the work item, "none" and "any" values are supported. |
|
||||
| <a id="namespaceworkitemsweightwildcardid"></a>`weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. |
|
||||
|
||||
##### `Namespace.workItemsWidgets`
|
||||
|
||||
List of available widgets for the given work items.
|
||||
|
||||
{{< details >}}
|
||||
**Introduced** in GitLab 18.2.
|
||||
**Status**: Experiment.
|
||||
{{< /details >}}
|
||||
|
||||
Returns [`[String!]`](#string).
|
||||
|
||||
###### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="namespaceworkitemswidgetsids"></a>`ids` | [`[WorkItemsTypeID!]!`](#workitemstypeid) | Global ID array of work items types to fetch available widgets for. A max of 100 IDs can be provided at a time. |
|
||||
|
||||
##### `Namespace.workspacesClusterAgents`
|
||||
|
||||
Cluster agents in the namespace with workspaces capabilities.
|
||||
|
@ -44316,6 +44380,7 @@ Possible item types for AI items.
|
|||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| <a id="aicatalogitemtypeagent"></a>`AGENT` | Agent. |
|
||||
| <a id="aicatalogitemtypeflow"></a>`FLOW` | Flow. |
|
||||
|
||||
### `AiConversationsThreadsConversationType`
|
||||
|
||||
|
@ -49880,6 +49945,7 @@ An AI catalog item.
|
|||
Implementations:
|
||||
|
||||
- [`AiCatalogAgent`](#aicatalogagent)
|
||||
- [`AiCatalogFlow`](#aicatalogflow)
|
||||
|
||||
##### Fields
|
||||
|
||||
|
@ -49900,6 +49966,7 @@ An AI catalog item version.
|
|||
Implementations:
|
||||
|
||||
- [`AiCatalogAgentVersion`](#aicatalogagentversion)
|
||||
- [`AiCatalogFlowVersion`](#aicatalogflowversion)
|
||||
|
||||
##### Fields
|
||||
|
||||
|
|
|
@ -68,7 +68,8 @@ Example response:
|
|||
"id": 58,
|
||||
"username": "service_account_group_346_<random_hash>",
|
||||
"name": "Service account user",
|
||||
"email": "service_account_group_346_<random_hash>@noreply.gitlab.example.com"
|
||||
"email": "service_account_group_346_<random_hash>@noreply.gitlab.example.com",
|
||||
"unconfirmed_email": "custom_email@example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -103,7 +104,7 @@ Supported attributes:
|
|||
| `id` | integer/string | yes | ID or [URL-encoded path](rest/_index.md#namespaced-paths) of a top-level group. |
|
||||
| `name` | string | no | User account name. If not specified, uses `Service account user`. |
|
||||
| `username` | string | no | User account username. If not specified, generates a name prepended with `service_account_group_`. |
|
||||
| `email` | string | no | User account email. If not specified, generates an email prepended with `service_account_group_`. Custom email addresses require confirmation before the account is active, unless the group has a matching [verified domain](../user/enterprise_user/_index.md#verified-domains-for-groups). |
|
||||
| `email` | string | no | Email of the user account. If not specified, generates an email prepended with `service_account_group_`. Custom email addresses require confirmation, unless the group has a matching [verified domain](../user/enterprise_user/_index.md#verified-domains-for-groups) or email confirmation settings are [turned off](../administration/settings/sign_up_restrictions.md#confirm-user-email). |
|
||||
|
||||
Example request:
|
||||
|
||||
|
@ -127,6 +128,7 @@ Example response:
|
|||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/182607/) in GitLab 17.10.
|
||||
- Add custom email address [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196309) in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
@ -150,11 +152,12 @@ Parameters:
|
|||
| `user_id` | integer | yes | The ID of the service account. |
|
||||
| `name` | string | no | Name of the user. |
|
||||
| `username` | string | no | Username of the user. |
|
||||
| `email` | string | no | Email of the user account. Custom email addresses require confirmation, unless the group has a matching [verified domain](../user/enterprise_user/_index.md#verified-domains-for-groups) or email confirmation settings are [turned off](../administration/settings/sign_up_restrictions.md#confirm-user-email). |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/345/service_accounts/57" --data "name=Updated Service Account"
|
||||
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/345/service_accounts/57" --data "name=Updated Service Account email=updated_email@example.com"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
@ -164,7 +167,8 @@ Example response:
|
|||
"id": 57,
|
||||
"username": "service_account_group_345_6018816a18e515214e0c34c2b33523fc",
|
||||
"name": "Updated Service Account",
|
||||
"email": "service_account_group_345_<random_hash>@noreply.gitlab.example.com"
|
||||
"email": "service_account_group_345_<random_hash>@noreply.gitlab.example.com",
|
||||
"unconfirmed_email": "custom_email@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -92,6 +92,12 @@ authorization with each flow.
|
|||
|
||||
### Authorization code with Proof Key for Code Exchange (PKCE)
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Group SAML SSO support for OAuth applications [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/461212) in GitLab 18.2 [with a flag](../administration/feature_flags/_index.md) named `ff_oauth_redirect_to_sso_login`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
The [PKCE RFC](https://www.rfc-editor.org/rfc/rfc7636#section-1.1) includes a
|
||||
detailed flow description, from authorization request through access token.
|
||||
The following steps describe our implementation of the flow.
|
||||
|
@ -120,7 +126,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
|
|||
`/oauth/authorize` page with the following query parameters:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES&code_challenge=CODE_CHALLENGE&code_challenge_method=S256&top_level_namespace_path=TOP_LEVEL_NAMESPACE
|
||||
```
|
||||
|
||||
This page asks the user to approve the request from the app to access their
|
||||
|
@ -128,6 +134,8 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
|
|||
redirected back to the specified `REDIRECT_URI`. The [scope parameter](../integration/oauth_provider.md#view-all-authorized-applications)
|
||||
is a space-separated list of scopes associated with the user.
|
||||
For example,`scope=read_user+profile` requests the `read_user` and `profile` scopes.
|
||||
The `top_level_namespace_path` is the top level namespace path associated with the project. This optional parameter
|
||||
should be used when [SAML SSO](../user/group/saml_sso/_index.md) is configured for the associated group.
|
||||
The redirect includes the authorization `code`, for example:
|
||||
|
||||
```plaintext
|
||||
|
@ -188,6 +196,12 @@ You can now make requests to the API with the access token.
|
|||
|
||||
### Authorization code flow
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Group SAML SSO support for OAuth applications [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/461212) in GitLab 18.2 [with a flag](../administration/feature_flags/_index.md) named `ff_oauth_redirect_to_sso_login`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
{{< alert type="note" >}}
|
||||
|
||||
Check the [RFC spec](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) for a
|
||||
|
@ -206,7 +220,7 @@ be used as a CSRF token.
|
|||
`/oauth/authorize` page with the following query parameters:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE&scope=REQUESTED_SCOPES&top_level_namespace_path=TOP_LEVEL_NAMESPACE
|
||||
```
|
||||
|
||||
This page asks the user to approve the request from the app to access their
|
||||
|
@ -214,6 +228,8 @@ be used as a CSRF token.
|
|||
redirected back to the specified `REDIRECT_URI`. The [scope parameter](../integration/oauth_provider.md#view-all-authorized-applications)
|
||||
is a space-separated list of scopes associated with the user.
|
||||
For example,`scope=read_user+profile` requests the `read_user` and `profile` scopes.
|
||||
The `top_level_namespace_path` is the top level namespace path associated with the project. This optional parameter
|
||||
should be used when [SAML SSO](../user/group/saml_sso/_index.md) is configured for the associated group.
|
||||
The redirect includes the authorization `code`, for example:
|
||||
|
||||
```plaintext
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -222,10 +222,20 @@ these parameters:
|
|||
- `delayed_project_deletion` and `delayed_group_deletion` attributes removed in GitLab 16.0.
|
||||
- `user_email_lookup_limit` attribute [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136886) in GitLab 16.7.
|
||||
- `default_branch_protection` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/408314) in GitLab 17.0. Use `default_branch_protection_defaults` instead.
|
||||
- `throttle_unauthenticated_git_http_enabled`, `throttle_unauthenticated_git_http_period_in_seconds`, and `throttle_unauthenticated_git_http_requests_per_period` attributes [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147112) in GitLab 17.0.
|
||||
- `allow_all_integrations` and `allowed_integrations` attributes [added](https://gitlab.com/gitlab-org/gitlab/-/issues/500610) in GitLab 17.6.
|
||||
- `throttle_authenticated_git_http_enabled`, `throttle_authenticated_git_http_period_in_seconds`, and `throttle_authenticated_git_http_requests_per_period` attributes [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/191552) in GitLab 18.1 [with a flag](../administration/feature_flags/_index.md) named `git_authenticated_http_limit`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
{{< alert type="flag" >}}
|
||||
|
||||
The availability of authenticated Git HTTP rate limits is controlled by a feature flag.
|
||||
For more information, see the history.
|
||||
This feature is available for testing, but not ready for production use.
|
||||
|
||||
{{< /alert >}}
|
||||
|
||||
Updates the current [application settings](#available-settings) for this GitLab instance.
|
||||
|
||||
```plaintext
|
||||
|
@ -754,6 +764,9 @@ to configure other related settings. These requirements are
|
|||
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires**: `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (for example, from crawlers or abusive bots). |
|
||||
| `throttle_authenticated_api_period_in_seconds` | integer | required by:<br>`throttle_authenticated_api_enabled` | Rate limit period (in seconds). |
|
||||
| `throttle_authenticated_api_requests_per_period` | integer | required by:<br>`throttle_authenticated_api_enabled` | Maximum requests per period per user. |
|
||||
| `throttle_authenticated_git_http_enabled` | boolean | conditionally | If `true`, enforces the authenticated Git HTTP request rate limit. Default value: `false`. |
|
||||
| `throttle_authenticated_git_http_period_in_seconds` | integer | no | Rate limit period in seconds. `throttle_authenticated_git_http_enabled` must be `true`. Default value: `3600`. |
|
||||
| `throttle_authenticated_git_http_requests_per_period` | integer | no | Maximum requests per period per user. `throttle_authenticated_git_http_enabled` must be `true`. Default value: `3600`. |
|
||||
| `throttle_authenticated_packages_api_enabled` | boolean | no | (**If enabled, requires**: `throttle_authenticated_packages_api_period_in_seconds` and `throttle_authenticated_packages_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (for example, from crawlers or abusive bots). View [package registry rate limits](../administration/settings/package_registry_rate_limits.md) for more details. |
|
||||
| `throttle_authenticated_packages_api_period_in_seconds` | integer | required by:<br>`throttle_authenticated_packages_api_enabled` | Rate limit period (in seconds). View [package registry rate limits](../administration/settings/package_registry_rate_limits.md) for more details. |
|
||||
| `throttle_authenticated_packages_api_requests_per_period` | integer | required by:<br>`throttle_authenticated_packages_api_enabled` | Maximum requests per period per user. View [package registry rate limits](../administration/settings/package_registry_rate_limits.md) for more details. |
|
||||
|
@ -766,6 +779,9 @@ to configure other related settings. These requirements are
|
|||
| `throttle_unauthenticated_api_enabled` | boolean | no | (**If enabled, requires**: `throttle_unauthenticated_api_period_in_seconds` and `throttle_unauthenticated_api_requests_per_period`) Enable unauthenticated API request rate limit. Helps reduce request volume (for example, from crawlers or abusive bots). |
|
||||
| `throttle_unauthenticated_api_period_in_seconds` | integer | required by:<br>`throttle_unauthenticated_api_enabled` | Rate limit period in seconds. |
|
||||
| `throttle_unauthenticated_api_requests_per_period` | integer | required by:<br>`throttle_unauthenticated_api_enabled` | Max requests per period per IP. |
|
||||
| `throttle_unauthenticated_git_http_enabled` | boolean | conditionally | If `true`, enforces the unauthenticated Git HTTP request rate limit. Default value: `false`. |
|
||||
| `throttle_unauthenticated_git_http_period_in_seconds` | integer | no | Rate limit period in seconds. `throttle_unauthenticated_git_http_enabled` must be `true`. Default value: `3600`. |
|
||||
| `throttle_unauthenticated_git_http_requests_per_period` | integer | no | Maximum requests per period per user. `throttle_unauthenticated_git_http_enabled` must be `true`. Default value: `3600`. |
|
||||
| `throttle_unauthenticated_packages_api_enabled` | boolean | no | (**If enabled, requires**: `throttle_unauthenticated_packages_api_period_in_seconds` and `throttle_unauthenticated_packages_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (for example, from crawlers or abusive bots). View [package registry rate limits](../administration/settings/package_registry_rate_limits.md) for more details. |
|
||||
| `throttle_unauthenticated_packages_api_period_in_seconds` | integer | required by:<br>`throttle_unauthenticated_packages_api_enabled` | Rate limit period (in seconds). View [package registry rate limits](../administration/settings/package_registry_rate_limits.md) for more details. |
|
||||
| `throttle_unauthenticated_packages_api_requests_per_period` | integer | required by:<br>`throttle_unauthenticated_packages_api_enabled` | Maximum requests per period per user. View [package registry rate limits](../administration/settings/package_registry_rate_limits.md) for more details. |
|
||||
|
|
|
@ -95,7 +95,7 @@ Supported attributes:
|
|||
| ---------- | ------ | -------- | ----------- |
|
||||
| `name` | string | no | Name of the user. If not set, uses `Service account user`. |
|
||||
| `username` | string | no | Username of the user account. If undefined, generates a name prepended with `service_account_`. |
|
||||
| `email` | string | no | Email of the user account. If undefined, generates a no-reply email address. |
|
||||
| `email` | string | no | Email of the user account. If undefined, generates a no-reply email address. Custom email addresses require confirmation, unless the email confirmation settings are [turned off](../administration/settings/sign_up_restrictions.md#confirm-user-email). |
|
||||
|
||||
Example request:
|
||||
|
||||
|
@ -116,3 +116,48 @@ Example response:
|
|||
|
||||
If the email address defined by the `email` attribute is already in use by another user,
|
||||
returns a `400 Bad request` error.
|
||||
|
||||
## Update an instance service account
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196309/) in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
Updates a specified instance service account.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have administrator access to the instance.
|
||||
|
||||
```plaintext
|
||||
PATCH /service_accounts/:id
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-----------|:---------------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `id` | integer | yes | ID of the service account. |
|
||||
| `name` | string | no | Name of the user. |
|
||||
| `username` | string | no | Username of the user account. |
|
||||
| `email` | string | no | Email of the user account. Custom email addresses require confirmation, unless the email confirmation settings are [turned off](../administration/settings/sign_up_restrictions.md#confirm-user-email). |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/service_accounts/57" --data "name=Updated Service Account email=updated_email@example.com"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 57,
|
||||
"username": "service_account_6018816a18e515214e0c34c2b33523fc",
|
||||
"name": "Updated Service Account",
|
||||
"email": "service_account_<random_hash>@noreply.gitlab.example.com",
|
||||
"unconfirmed_email": "custom_email@example.com"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -113,7 +113,7 @@ be embedded into incidents making problem resolving easier. Additionally, it can
|
|||
|
||||
If you use the GitLab CI Pipelines Exporter, you should start with the [example configuration](https://github.com/mvisonneau/gitlab-ci-pipelines-exporter/blob/main/docs/configuration_syntax.md).
|
||||
|
||||

|
||||

|
||||
|
||||
Alternatively, you can use a monitoring tool that can execute scripts, like
|
||||
[`check_gitlab`](https://gitlab.com/6uellerBpanda/check_gitlab) for example.
|
||||
|
|
|
@ -165,7 +165,7 @@ curl --request POST \
|
|||
CI/CD variables in triggered pipelines display on each job's page, but only
|
||||
users with the Owner and Maintainer role can view the values.
|
||||
|
||||

|
||||

|
||||
|
||||
Using inputs to control pipeline behavior offers improved security and flexibility over CI/CD variables.
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ Store the exit code in a variable to avoid this behavior:
|
|||
```yaml
|
||||
job:
|
||||
script:
|
||||
- exit_code=0
|
||||
- false || exit_code=$?
|
||||
- if [ $exit_code -ne 0 ]; then echo "Previous command failed"; fi;
|
||||
```
|
||||
|
|
|
@ -91,7 +91,7 @@ this information to help analyze a vulnerability.
|
|||
|
||||
The following tips may also help you analyze a vulnerability:
|
||||
|
||||
- Use [GitLab Duo Vulnerability Explanation](../vulnerabilities/_index.md#explaining-a-vulnerability)
|
||||
- Use [GitLab Duo Vulnerability Explanation](../vulnerabilities/_index.md#vulnerability-explanation)
|
||||
to help explain the vulnerability and suggest a remediation. Available only for vulnerabilities
|
||||
detected by SAST.
|
||||
- Use [security training](../vulnerabilities/_index.md#view-security-training-for-a-vulnerability)
|
||||
|
|
|
@ -47,7 +47,7 @@ effective.
|
|||
|
||||
For some vulnerabilities detected by SAST, GitLab can:
|
||||
|
||||
- [Explain the vulnerability](../vulnerabilities/_index.md#explaining-a-vulnerability), using GitLab
|
||||
- [Explain the vulnerability](../vulnerabilities/_index.md#vulnerability-explanation), using GitLab
|
||||
Duo Chat.
|
||||
- [Resolve the vulnerability](../vulnerabilities/_index.md#vulnerability-resolution), using GitLab
|
||||
Duo Chat.
|
||||
|
|
|
@ -42,7 +42,7 @@ If you're comparing GitLab SAST to another product, you may find that some of it
|
|||
- [Secret detection](../secret_detection/_index.md) finds leaked secrets in your code.
|
||||
- [Security policies](../policies/_index.md) allow you to force scans to run or require that vulnerabilities are fixed.
|
||||
- [Vulnerability management and reporting](../vulnerability_report/_index.md) manages the vulnerabilities that exist in the codebase and integrates with issue trackers.
|
||||
- GitLab Duo [vulnerability explanation](../vulnerabilities/_index.md#explaining-a-vulnerability) and [vulnerability resolution](../vulnerabilities/_index.md#resolve-a-vulnerability) help you remediate vulnerabilities quickly by using AI.
|
||||
- GitLab Duo [vulnerability explanation](../vulnerabilities/_index.md#vulnerability-explanation) and [vulnerability resolution](../vulnerabilities/_index.md#vulnerability-resolution) help you remediate vulnerabilities quickly by using AI.
|
||||
|
||||
## Choose a test codebase
|
||||
|
||||
|
@ -98,5 +98,5 @@ After you choose a codebase to test with, you're ready to conduct the test. You
|
|||
- If you're using GitLab Advanced SAST, you can use the [Scanner filter](../vulnerability_report/_index.md#scanner-filter) to show results only from that scanner.
|
||||
1. Review vulnerability results.
|
||||
- Check the [code flow view](../vulnerabilities/_index.md#vulnerability-code-flow) for GitLab Advanced SAST vulnerabilities that involve tainted user input, like SQL injection or path traversal.
|
||||
- If you have GitLab Duo Enterprise, [explain](../vulnerabilities/_index.md#explaining-a-vulnerability) or [resolve](../vulnerabilities/_index.md#resolve-a-vulnerability) a vulnerability.
|
||||
- If you have GitLab Duo Enterprise, [explain](../vulnerabilities/_index.md#vulnerability-explanation) or [resolve](../vulnerabilities/_index.md#vulnerability-resolution) a vulnerability.
|
||||
1. To see how scanning works as new code is developed, create a new merge request that changes application code and adds a new vulnerability or weakness.
|
||||
|
|
|
@ -38,7 +38,7 @@ For further details on this additional data, see [vulnerability risk assessment
|
|||
If the scanner determined the vulnerability to be a false positive, an alert message is included at
|
||||
the top of the vulnerability's page.
|
||||
|
||||
## Explaining a vulnerability
|
||||
## Vulnerability Explanation
|
||||
|
||||
{{< details >}}
|
||||
|
||||
|
@ -59,7 +59,7 @@ the top of the vulnerability's page.
|
|||
|
||||
{{< /history >}}
|
||||
|
||||
GitLab can help you with a vulnerability by using a large language model to:
|
||||
GitLab Duo Vulnerability Explanation can help you with a vulnerability by using a large language model to:
|
||||
|
||||
- Summarize the vulnerability.
|
||||
- Help developers and security analysts to understand the vulnerability, how it could be exploited, and how to fix it.
|
||||
|
@ -67,11 +67,6 @@ GitLab can help you with a vulnerability by using a large language model to:
|
|||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch an overview](https://www.youtube.com/watch?v=MMVFvGrmMzw&list=PLFGfElNsQthZGazU1ZdfDpegu0HflunXW)
|
||||
|
||||
### Vulnerability Explanation
|
||||
|
||||
Explain a vulnerability with GitLab Duo Vulnerability Explanation. Use the explanation to better
|
||||
understand a vulnerability and its possible mitigation.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have the GitLab Ultimate subscription tier.
|
||||
|
|
|
@ -64,7 +64,7 @@ Follow this path to learn how to:
|
|||
- Automatically generate fix suggestions
|
||||
- Create merge requests to address security issues
|
||||
|
||||
[Start here: Vulnerability explanation and resolution →](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability)
|
||||
[Start here: Vulnerability explanation and resolution →](../application_security/vulnerabilities/_index.md#vulnerability-explanation)
|
||||
|
||||
{{< /tab >}}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ are available on GitLab.com and GitLab Self-Managed only.
|
|||
| [Code Review](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes |
|
||||
| [Discussion Summary](../discussions/_index.md#summarize-issue-discussions-with-duo-chat) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes |
|
||||
| [Root Cause Analysis](../gitlab_duo_chat/examples.md#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes |
|
||||
| [Vulnerability Explanation](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes |
|
||||
| [Vulnerability Explanation](../application_security/vulnerabilities/_index.md#vulnerability-explanation) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes |
|
||||
| [Vulnerability Resolution](../application_security/vulnerabilities/_index.md#vulnerability-resolution) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes |
|
||||
| [GitLab Duo for the CLI](../../editor_extensions/gitlab_cli/_index.md#gitlab-duo-for-the-cli) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No |
|
||||
| [Merge Commit Message Generation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No |
|
||||
|
@ -45,6 +45,12 @@ and the status of those features, see the
|
|||
|
||||
## Beta and experimental features
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- GitLab Duo Agentic Chat added in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
The following features are not generally available.
|
||||
|
||||
They require a Premium or Ultimate subscription and one of the available add-ons.
|
||||
|
@ -52,7 +58,8 @@ They require a Premium or Ultimate subscription and one of the available add-ons
|
|||
| Feature | GitLab Duo Core | GitLab Duo Pro | GitLab Duo Enterprise | GitLab Duo with Amazon Q | GitLab.com | GitLab Self-Managed | GitLab Dedicated | GitLab Duo Self-Hosted |
|
||||
|---------|----------|---------|----------------|--------------------------|-----------|-------------|-----------|------------------------|
|
||||
| [Code Review Summary](../project/merge_requests/duo_in_merge_requests.md#summarize-a-code-review) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No | Experiment | Experiment | {{< icon name="dash-circle" >}} No | Experiment |
|
||||
| [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No | Experiment | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | N/A |
|
||||
| [Issue Description Generation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No | Experiment | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | Not applicable |
|
||||
| [Merge Request Summary](../project/merge_requests/duo_in_merge_requests.md#generate-a-description-by-summarizing-code-changes) | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No | Beta | Beta | {{< icon name="dash-circle" >}} No | Beta |
|
||||
| [GitLab Duo Agentic Chat](../gitlab_duo_chat/agentic_chat.md) | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="check-circle-filled" >}} Yes | {{< icon name="dash-circle" >}} No | Experiment | Experiment | {{< icon name="dash-circle" >}} No | {{< icon name="dash-circle" >}} No |
|
||||
|
||||
[GitLab Duo Agent Platform](../duo_agent_platform/_index.md) is in private beta, does not require an add-on, and is not supported for GitLab Duo Self-Hosted.
|
||||
|
|
|
@ -555,7 +555,7 @@ introduces a security vulnerability with a [buffer overflow](https://en.wikipedi
|
|||
printf("Contents of region: %s\n", region);
|
||||
```
|
||||
|
||||
[SAST security scanners](../application_security/sast/analyzers.md) can detect and report the problem. Use [Vulnerability Explanation](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability) to understand the problem.
|
||||
[SAST security scanners](../application_security/sast/analyzers.md) can detect and report the problem. Use [Vulnerability Explanation](../application_security/vulnerabilities/_index.md#vulnerability-explanation) to understand the problem.
|
||||
Vulnerability Resolution helps to generate an MR.
|
||||
If the suggested changes do not fit requirements, or might lead to problems, you can use Code Suggestions and Chat to refine. For example:
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ title: GitLab Duo Agentic Chat
|
|||
- Tier: Premium, Ultimate
|
||||
- Add-on: GitLab Duo Core, Pro, or Enterprise
|
||||
- Offering: GitLab.com, GitLab Self-Managed
|
||||
- Status: Beta
|
||||
- Status: Experiment
|
||||
- LLMs: Anthropic [Claude Sonnet 4](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-sonnet-4)
|
||||
|
||||
{{< /details >}}
|
||||
|
@ -21,7 +21,6 @@ title: GitLab Duo Agentic Chat
|
|||
- GitLab Duo Agentic Chat on VS Code [enabled on GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196688) in GitLab 18.2.
|
||||
- GitLab Duo Agentic Chat in the GitLab UI [introduced on GitLab.com and GitLab Self-Managed](https://gitlab.com/gitlab-org/gitlab/-/issues/546140) in GitLab 18.2 [with flags](../../administration/feature_flags/_index.md) named `duo_workflow_workhorse` and `duo_workflow_web_chat_mutation_tools`. Both flags are enabled by default.
|
||||
- Feature flag `duo_agentic_chat` enabled by default in GitLab 18.2.
|
||||
- GitLab Agentic Chat changed to beta in GitLab 18.2.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
|
@ -691,7 +691,7 @@ To troubleshoot a failed CI/CD job from the job log:
|
|||
|
||||
You can ask GitLab Duo Chat to explain a vulnerability when you are viewing a SAST vulnerability report.
|
||||
|
||||
For more information, see [Explaining a vulnerability](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability).
|
||||
For more information, see [Explaining a vulnerability](../application_security/vulnerabilities/_index.md#vulnerability-explanation).
|
||||
|
||||
## Create a new conversation
|
||||
|
||||
|
@ -794,7 +794,7 @@ These commands are dynamic and are available only in the GitLab UI when using Du
|
|||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | ---- |
|
||||
| /summarize_comments | Generate a summary of all comments on the current issue | Issues |
|
||||
| /troubleshoot | [Troubleshoot failed CI/CD jobs with Root Cause Analysis](#troubleshoot-failed-cicd-jobs-with-root-cause-analysis) | Jobs |
|
||||
| /vulnerability_explain | [Explain current vulnerability](../application_security/vulnerabilities/_index.md#explaining-a-vulnerability) | Vulnerabilities |
|
||||
| /vulnerability_explain | [Explain current vulnerability](../application_security/vulnerabilities/_index.md#vulnerability-explanation) | Vulnerabilities |
|
||||
| /new | [Create a new Chat conversation](_index.md#have-multiple-conversations-with-chat). GitLab 17.10 and later. | All |
|
||||
|
||||
### IDE
|
||||
|
|
|
@ -16,7 +16,8 @@ title: Immutable container tags
|
|||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/523276) as an [experiment](../../../policy/development_stages_support.md) in GitLab 18.1 [with a flag](../../../administration/feature_flags/_index.md) named `container_registry_immutable_tags`. Disabled by default.
|
||||
- [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/523276) in GitLab 18.1.
|
||||
- [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/523276) in GitLab 18.2.
|
||||
- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/523276) in GitLab 18.2. Feature flag `container_registry_immutable_tags` removed.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
|
|
|
@ -246,7 +246,7 @@ You can also use the `/submit_review` [quick action](../../quick_actions.md) in
|
|||
|
||||
A reviewer [requesting changes](#submit-a-review) blocks a merge request from merging.
|
||||
When this happens, the merge request reports area shows the message
|
||||
**The change requests must be completed or resolved**. To unblock the merge request,
|
||||
**Change requests must be approved by the requesting user**. To unblock the merge request,
|
||||
the reviewer who requested changes should [re-review and approve](#re-request-a-review) the merge request.
|
||||
|
||||
### Remove a change request
|
||||
|
@ -269,7 +269,7 @@ To remove your change request without submitting a new review:
|
|||
1. Select **Code > Merge requests** and find your merge request.
|
||||
1. Select the title of the merge request to view it.
|
||||
1. On the merge request **Overview**, scroll to the merge request reports area.
|
||||
1. Next to **The change requests must be completed or resolved**, select **Remove**:
|
||||
1. Next to **Change requests must be approved by the requesting user**, select **Remove**:
|
||||
|
||||

|
||||
|
||||
|
@ -282,14 +282,14 @@ another user with permission to merge the merge request can override this check:
|
|||
1. Select **Code > Merge requests** and find your merge request.
|
||||
1. Select the title of the merge request to view it.
|
||||
1. On the merge request **Overview**, scroll to the merge request reports area.
|
||||
1. Next to **The change requests must be completed or resolved**, select **Bypass**:
|
||||
1. Next to **Change requests must be approved by the requesting user**, select **Bypass**:
|
||||
|
||||

|
||||
|
||||
1. The merge reports area shows `Merge with caution: Override added`. To see which check a user
|
||||
bypassed, select **Expand merge checks** ({{< icon name="chevron-lg-down" >}}) and find the
|
||||
check that contains a warning ({{< icon name="status_warning" >}}) icon. In this example, the
|
||||
author bypassed **The change requests must be completed or resolved**:
|
||||
author bypassed **Change requests must be approved by the requesting user**:
|
||||
|
||||

|
||||
|
||||
|
|
|
@ -12,6 +12,12 @@ title: GitLab Pages access control
|
|||
|
||||
{{< /details >}}
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- Group SAML SSO support for Pages [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326288) in GitLab 18.2 [with a flag](../../../administration/feature_flags/_index.md) named `ff_oauth_redirect_to_sso_login`. Disabled by default.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
You can enable Pages access control on your project
|
||||
if your administrator has [enabled the access control feature](../../../administration/pages/_index.md#access-control)
|
||||
on your GitLab instance. When enabled, only authenticated
|
||||
|
@ -49,6 +55,9 @@ The next time someone tries to access your website and the access control is
|
|||
enabled, they're presented with a page to sign in to GitLab and verify they
|
||||
can access the website.
|
||||
|
||||
When [SAML SSO](../../group/saml_sso/_index.md) is configured for the associated group
|
||||
and the access control is enabled, users must authenticate using SSO before accessing the website.
|
||||
|
||||
## Restrict Pages access to project members for the group and its subgroups
|
||||
|
||||
{{< history >}}
|
||||
|
|
|
@ -19,6 +19,16 @@ module Gitlab
|
|||
too_long: 'has too many items (maximum is %{count})'
|
||||
}
|
||||
end
|
||||
|
||||
def value
|
||||
config.map do |file_path|
|
||||
if file_path.start_with?('/')
|
||||
file_path.sub(%r{^/+}, '')
|
||||
else
|
||||
file_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,15 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def namespace_id_for_new_entity(new_entity)
|
||||
case new_entity
|
||||
when Issue
|
||||
new_entity.namespace_id
|
||||
else
|
||||
raise StandardError, "Copying resource events for #{new_entity.class.name} is not supported yet"
|
||||
end
|
||||
end
|
||||
|
||||
def copy_resource_label_events
|
||||
copy_events(ResourceLabelEvent.table_name, original_entity.resource_label_events) do |event|
|
||||
event.attributes
|
||||
|
@ -39,11 +48,16 @@ module Gitlab
|
|||
def copy_resource_state_events
|
||||
return unless state_events_supported?
|
||||
|
||||
new_namespace_id = namespace_id_for_new_entity(new_entity)
|
||||
|
||||
copy_events(ResourceStateEvent.table_name, original_entity.resource_state_events) do |event|
|
||||
event.attributes
|
||||
.except(*blocked_resource_event_attributes)
|
||||
.merge(entity_key => new_entity.id,
|
||||
'state' => ResourceStateEvent.states[event.state])
|
||||
.merge(
|
||||
entity_key => new_entity.id,
|
||||
'state' => ResourceStateEvent.states[event.state],
|
||||
'namespace_id' => new_namespace_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12581,6 +12581,9 @@ msgstr ""
|
|||
msgid "Change path"
|
||||
msgstr ""
|
||||
|
||||
msgid "Change requests must be approved by the requesting user."
|
||||
msgstr ""
|
||||
|
||||
msgid "Change reviewers"
|
||||
msgstr ""
|
||||
|
||||
|
@ -41492,6 +41495,9 @@ msgstr ""
|
|||
msgid "No more than %{max_frameworks} compliance frameworks can be updated at the same time."
|
||||
msgstr ""
|
||||
|
||||
msgid "No more than %{max_work_items} work items can be loaded at the same time"
|
||||
msgstr ""
|
||||
|
||||
msgid "No more than %{max_work_items} work items can be modified at the same time."
|
||||
msgstr ""
|
||||
|
||||
|
@ -58508,6 +58514,12 @@ msgstr ""
|
|||
msgid "ServiceAccounts|The service account was updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "ServiceAccount|Group ID provided does not match the service account's group ID."
|
||||
msgstr ""
|
||||
|
||||
msgid "ServiceAccount|Group with the provided ID not found."
|
||||
msgstr ""
|
||||
|
||||
msgid "ServiceAccount|No more seats are available to create Service Account User"
|
||||
msgstr ""
|
||||
|
||||
|
@ -58520,6 +58532,12 @@ msgstr ""
|
|||
msgid "ServiceAccount|User does not have permission to delete a service account."
|
||||
msgstr ""
|
||||
|
||||
msgid "ServiceAccount|User does not have permission to update a service account."
|
||||
msgstr ""
|
||||
|
||||
msgid "ServiceAccount|User is not a service account"
|
||||
msgstr ""
|
||||
|
||||
msgid "ServiceAccount|You are not authorized to update service accounts in this namespace."
|
||||
msgstr ""
|
||||
|
||||
|
@ -61886,6 +61904,9 @@ msgstr ""
|
|||
msgid "Target branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target group prevents forks that point outside this group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target project cannot be equal to source project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -62547,9 +62568,6 @@ msgstr ""
|
|||
msgid "The branch to merge into."
|
||||
msgstr ""
|
||||
|
||||
msgid "The change requests must be completed or resolved."
|
||||
msgstr ""
|
||||
|
||||
msgid "The comment you are editing has been changed by another user. Would you like to keep your changes and overwrite the new description or discard your changes?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -67317,9 +67335,6 @@ msgstr ""
|
|||
msgid "User is blocked"
|
||||
msgstr ""
|
||||
|
||||
msgid "User is not a service account"
|
||||
msgstr ""
|
||||
|
||||
msgid "User is not allowed to resolve thread"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/query-language-rust": "0.12.0",
|
||||
"@gitlab/svgs": "3.138.0",
|
||||
"@gitlab/ui": "114.8.1",
|
||||
"@gitlab/ui": "115.0.1",
|
||||
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.1",
|
||||
"@gitlab/vuex-vue3": "npm:vuex@4.1.0",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20250704091020",
|
||||
|
|
|
@ -107,13 +107,5 @@ RSpec.describe RecordUserLastActivity, feature_category: :seat_cost_management d
|
|||
it_behaves_like 'does not update publish an activity event'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the request is not a GET request' do
|
||||
before do
|
||||
allow(controller.request).to receive(:get?).and_return(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not update publish an activity event'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,16 +41,16 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
|
||||
it 'publishes activity events accordingly' do
|
||||
if container.is_a?(Project)
|
||||
if container.is_a?(PersonalSnippet)
|
||||
expect { get :info_refs, params: params }
|
||||
.not_to publish_event(Users::ActivityEvent)
|
||||
else
|
||||
expect { get :info_refs, params: params }
|
||||
.to publish_event(Users::ActivityEvent)
|
||||
.with({
|
||||
user_id: user.id,
|
||||
namespace_id: project.root_ancestor.id
|
||||
})
|
||||
else
|
||||
expect { get :info_refs, params: params }
|
||||
.not_to publish_event(Users::ActivityEvent)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,44 +5,47 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
|
|||
<div
|
||||
class="gl-tabs tabs"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="gl-tabs-wrapper"
|
||||
>
|
||||
<div
|
||||
class="gl-actions-tabs-start"
|
||||
data-testid="actions-tabs-start"
|
||||
role="toolbar"
|
||||
>
|
||||
<a
|
||||
class="btn btn-confirm btn-md gl-button"
|
||||
data-testid="action-primary"
|
||||
href="/new"
|
||||
>
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
New custom emoji
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<ul
|
||||
class="gl-tabs-nav nav"
|
||||
role="tablist"
|
||||
/>
|
||||
<div
|
||||
class="gl-actions-tabs-end"
|
||||
data-testid="actions-tabs-end"
|
||||
role="toolbar"
|
||||
>
|
||||
<div
|
||||
class="gl-actions-tabs-start"
|
||||
data-testid="actions-tabs-start"
|
||||
<a
|
||||
class="btn btn-confirm btn-md gl-button"
|
||||
data-testid="action-primary"
|
||||
href="/new"
|
||||
>
|
||||
<a
|
||||
class="btn btn-confirm btn-md gl-button"
|
||||
data-testid="action-primary"
|
||||
href="/new"
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
New custom emoji
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="gl-actions-tabs-end"
|
||||
data-testid="actions-tabs-end"
|
||||
>
|
||||
<a
|
||||
class="btn btn-confirm btn-md gl-button"
|
||||
data-testid="action-primary"
|
||||
href="/new"
|
||||
>
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
New custom emoji
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</ul>
|
||||
New custom emoji
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gl-pt-0 gl-tab-content tab-content"
|
||||
|
|
|
@ -31,6 +31,7 @@ import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/h
|
|||
import LineHighlighter from '~/blob/line_highlighter';
|
||||
import { LEGACY_FILE_TYPES } from '~/repository/constants';
|
||||
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
|
||||
import eventHub from '~/notes/event_hub';
|
||||
import {
|
||||
simpleViewerMock,
|
||||
richViewerMock,
|
||||
|
@ -277,6 +278,28 @@ describe('Blob content viewer component', () => {
|
|||
},
|
||||
);
|
||||
|
||||
describe('code navigation', () => {
|
||||
const setup = async (viewer, viewerType) => {
|
||||
jest.spyOn(eventHub, '$emit').mockImplementation();
|
||||
mockAxios
|
||||
.onGet(`/some_file.js?format=json&viewer=${viewerType}`)
|
||||
.replyOnce(HTTP_STATUS_OK, 'test');
|
||||
await createComponent({ blob: viewer });
|
||||
};
|
||||
|
||||
it('emits showBlobInteractionZones for text files', async () => {
|
||||
await setup(simpleViewerMock, 'simple');
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', 'some_file.js');
|
||||
});
|
||||
|
||||
it('does not emit showBlobInteractionZones non-text files', async () => {
|
||||
await setup(richViewerMock, 'rich');
|
||||
|
||||
expect(eventHub.$emit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the LineHighlighter', async () => {
|
||||
mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test');
|
||||
await createComponent({ blob: { ...simpleViewerMock, fileType } });
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import FileTreeBrowser, { TREE_WIDTH } from '~/repository/file_tree_browser/file_tree_browser.vue';
|
||||
import FileTreeBrowser, {
|
||||
TREE_WIDTH,
|
||||
FILE_TREE_BROWSER_STORAGE_KEY,
|
||||
} from '~/repository/file_tree_browser/file_tree_browser.vue';
|
||||
import FileBrowserHeight from '~/diffs/components/file_browser_height.vue';
|
||||
import TreeList from '~/repository/file_tree_browser/components/tree_list.vue';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
|
||||
describe('FileTreeBrowser', () => {
|
||||
let wrapper;
|
||||
|
||||
useLocalStorageSpy();
|
||||
|
||||
const findFileBrowserHeight = () => wrapper.findComponent(FileBrowserHeight);
|
||||
const findTreeList = () => wrapper.findComponent(TreeList);
|
||||
const findPanelResizer = () => wrapper.findComponent(PanelResizer);
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const createComponent = (routeName = 'blobPathDecoded') => {
|
||||
wrapper = shallowMount(FileTreeBrowser, {
|
||||
|
@ -29,12 +41,53 @@ describe('FileTreeBrowser', () => {
|
|||
|
||||
it('renders the file browser height component', () => {
|
||||
expect(findFileBrowserHeight().exists()).toBe(true);
|
||||
expect(findFileBrowserHeight().attributes('style')).toBe(`width: ${TREE_WIDTH}px;`);
|
||||
expect(findFileBrowserHeight().attributes('style')).toBe(`--tree-width: ${TREE_WIDTH}px;`);
|
||||
});
|
||||
|
||||
it('renders the tree list component', () => {
|
||||
expect(findTreeList().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('PanelResizer component', () => {
|
||||
it('renders the panel resizer component', () => {
|
||||
expect(findPanelResizer().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('updates tree width when panel resizer emits update:size', async () => {
|
||||
const newWidth = 400;
|
||||
|
||||
await findPanelResizer().vm.$emit('update:size', newWidth);
|
||||
|
||||
expect(findFileBrowserHeight().attributes('style')).toBe(`--tree-width: ${newWidth}px;`);
|
||||
});
|
||||
|
||||
it('saves tree width preference when panel resizer emits resize-end', async () => {
|
||||
const newWidth = 400;
|
||||
|
||||
await findPanelResizer().vm.$emit('resize-end', newWidth);
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(FILE_TREE_BROWSER_STORAGE_KEY, newWidth);
|
||||
expect(findFileBrowserHeight().attributes('style')).toBe(`--tree-width: ${newWidth}px;`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localStorage handling', () => {
|
||||
it('restores tree width from localStorage on component creation', () => {
|
||||
const storedWidth = 350;
|
||||
localStorage.setItem(FILE_TREE_BROWSER_STORAGE_KEY, storedWidth.toString());
|
||||
|
||||
createComponent();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith(FILE_TREE_BROWSER_STORAGE_KEY);
|
||||
expect(findFileBrowserHeight().attributes('style')).toBe(`--tree-width: ${storedWidth}px;`);
|
||||
});
|
||||
|
||||
it('uses default width when localStorage is empty', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findFileBrowserHeight().attributes('style')).toBe(`--tree-width: ${TREE_WIDTH}px;`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when on project overview page', () => {
|
||||
|
@ -47,5 +100,9 @@ describe('FileTreeBrowser', () => {
|
|||
it('does not render the tree list component', () => {
|
||||
expect(findTreeList().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the panel resizer component', () => {
|
||||
expect(findPanelResizer().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('Merge request merge checks message component', () => {
|
|||
${'merge_request_blocked'} | ${'Merge request dependencies must be merged.'}
|
||||
${'status_checks_must_pass'} | ${'Status checks must pass.'}
|
||||
${'jira_association_missing'} | ${'Either the title or description must reference a Jira issue.'}
|
||||
${'requested_changes'} | ${'The change requests must be completed or resolved.'}
|
||||
${'requested_changes'} | ${'Change requests must be approved by the requesting user.'}
|
||||
${'approvals_syncing'} | ${'The merge request approvals are currently syncing.'}
|
||||
${'locked_paths'} | ${'All paths must be unlocked'}
|
||||
${'locked_lfs_files'} | ${'All LFS files must be unlocked.'}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Resolvers::WorkItems::WidgetsResolver, feature_category: :team_planning do
|
||||
include GraphqlHelpers
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:epic_type) { create(:work_item_type, :epic, namespace: group) }
|
||||
let_it_be(:issue_type) { create(:work_item_type, :issue, namespace: group) }
|
||||
let_it_be(:task_type) { create(:work_item_type, :task, namespace: group) }
|
||||
|
||||
def resolve_items(args = {}, context = { current_user: current_user })
|
||||
resolve(described_class, args: args, ctx: context, arg_style: :internal, obj: group)
|
||||
end
|
||||
|
||||
it 'when passing more work items than limit' do
|
||||
expect_graphql_error_to_be_created(
|
||||
Gitlab::Graphql::Errors::ArgumentError,
|
||||
'No more than 100 work items can be loaded at the same time'
|
||||
) do
|
||||
resolve_items(ids: (0..105).to_a.map { |id| "gid://gitlab/WorkItem/#{id}" })
|
||||
end
|
||||
end
|
||||
|
||||
it 'when ids do not exist' do
|
||||
expect(resolve_items(ids: [GlobalID.parse("gid://gitlab/WorkItem/-1")]))
|
||||
.to be_empty
|
||||
end
|
||||
|
||||
where(:work_item_types, :widgets) do
|
||||
[
|
||||
[
|
||||
lazy { [epic_type] },
|
||||
%w[
|
||||
ASSIGNEES
|
||||
AWARD_EMOJI
|
||||
CURRENT_USER_TODOS
|
||||
DESCRIPTION
|
||||
HIERARCHY
|
||||
LABELS
|
||||
LINKED_ITEMS
|
||||
MILESTONE
|
||||
NOTES
|
||||
NOTIFICATIONS
|
||||
PARTICIPANTS
|
||||
START_AND_DUE_DATE
|
||||
TIME_TRACKING
|
||||
]
|
||||
],
|
||||
[
|
||||
lazy { [issue_type] },
|
||||
%w[
|
||||
ASSIGNEES
|
||||
AWARD_EMOJI
|
||||
CRM_CONTACTS
|
||||
CURRENT_USER_TODOS
|
||||
DESCRIPTION
|
||||
DESIGNS
|
||||
DEVELOPMENT
|
||||
EMAIL_PARTICIPANTS
|
||||
ERROR_TRACKING
|
||||
HIERARCHY
|
||||
LABELS
|
||||
LINKED_ITEMS
|
||||
LINKED_RESOURCES
|
||||
MILESTONE
|
||||
NOTES
|
||||
NOTIFICATIONS
|
||||
PARTICIPANTS
|
||||
START_AND_DUE_DATE
|
||||
TIME_TRACKING
|
||||
]
|
||||
],
|
||||
[
|
||||
lazy { [task_type] },
|
||||
%w[
|
||||
ASSIGNEES
|
||||
AWARD_EMOJI
|
||||
CRM_CONTACTS
|
||||
CURRENT_USER_TODOS
|
||||
DESCRIPTION
|
||||
DEVELOPMENT
|
||||
HIERARCHY
|
||||
LABELS
|
||||
LINKED_ITEMS
|
||||
LINKED_RESOURCES
|
||||
MILESTONE
|
||||
NOTES
|
||||
NOTIFICATIONS
|
||||
PARTICIPANTS
|
||||
START_AND_DUE_DATE
|
||||
TIME_TRACKING
|
||||
]
|
||||
],
|
||||
[
|
||||
lazy { [epic_type, issue_type] },
|
||||
%w[
|
||||
ASSIGNEES
|
||||
AWARD_EMOJI
|
||||
CRM_CONTACTS
|
||||
CURRENT_USER_TODOS
|
||||
DESCRIPTION
|
||||
DESIGNS
|
||||
DEVELOPMENT
|
||||
EMAIL_PARTICIPANTS
|
||||
ERROR_TRACKING
|
||||
HIERARCHY
|
||||
LABELS
|
||||
LINKED_ITEMS
|
||||
LINKED_RESOURCES
|
||||
MILESTONE
|
||||
NOTES
|
||||
NOTIFICATIONS
|
||||
PARTICIPANTS
|
||||
START_AND_DUE_DATE
|
||||
TIME_TRACKING
|
||||
]
|
||||
],
|
||||
[
|
||||
lazy { [epic_type, issue_type, task_type] },
|
||||
%w[
|
||||
ASSIGNEES
|
||||
AWARD_EMOJI
|
||||
CRM_CONTACTS
|
||||
CURRENT_USER_TODOS
|
||||
DESCRIPTION
|
||||
DESIGNS
|
||||
DEVELOPMENT
|
||||
EMAIL_PARTICIPANTS
|
||||
ERROR_TRACKING
|
||||
HIERARCHY
|
||||
LABELS
|
||||
LINKED_ITEMS
|
||||
LINKED_RESOURCES
|
||||
MILESTONE
|
||||
NOTES
|
||||
NOTIFICATIONS
|
||||
PARTICIPANTS
|
||||
START_AND_DUE_DATE
|
||||
TIME_TRACKING
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
with_them do
|
||||
it "list unique widgets for the given work items" do
|
||||
expect(resolve_items(ids: work_item_types.map(&:to_gid))).to match_array(widgets)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['Namespace'] do
|
||||
RSpec.describe GitlabSchema.types['Namespace'], feature_category: :shared do
|
||||
specify { expect(described_class.graphql_name).to eq('Namespace') }
|
||||
|
||||
specify { expect(described_class.interfaces).to include(Types::TodoableInterface) }
|
||||
|
@ -12,7 +12,8 @@ RSpec.describe GitlabSchema.types['Namespace'] do
|
|||
id name path full_name full_path achievements_path description description_html visibility
|
||||
lfs_enabled request_access_enabled projects root_storage_statistics shared_runners_setting
|
||||
timelog_categories achievements work_item pages_deployments import_source_users work_item_types
|
||||
sidebar work_item_description_templates ci_cd_settings avatar_url link_paths licensed_features
|
||||
work_items_widgets sidebar work_item_description_templates ci_cd_settings avatar_url link_paths
|
||||
licensed_features
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -22,6 +22,64 @@ RSpec.describe Gitlab::Ci::Config::Entry::Files do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when entry config contains absolute paths' do
|
||||
let(:config) { ['/absolute/path', 'relative/path'] }
|
||||
|
||||
describe '#value' do
|
||||
it 'strips leading slashes from absolute paths' do
|
||||
expect(entry.value).to match_array ['absolute/path', 'relative/path']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry config contains only absolute paths' do
|
||||
let(:config) { ['/tmp/test.txt', '/var/log/app.log'] }
|
||||
|
||||
describe '#value' do
|
||||
it 'strips leading slashes from all paths' do
|
||||
expect(entry.value).to match_array ['tmp/test.txt', 'var/log/app.log']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry config contains paths with multiple leading slashes' do
|
||||
let(:config) { ['//double/slash', '///triple/slash'] }
|
||||
|
||||
describe '#value' do
|
||||
it 'strips all leading slashes' do
|
||||
expect(entry.value).to match_array ['double/slash', 'triple/slash']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry config contains empty strings' do
|
||||
let(:config) { ['', 'valid/path'] }
|
||||
|
||||
describe '#value' do
|
||||
it 'preserves empty strings' do
|
||||
expect(entry.value).to match_array ['', 'valid/path']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#errors' do
|
||||
context 'when entry value is not an array' do
|
||||
let(:config) { 'string' }
|
||||
|
|
|
@ -9,8 +9,9 @@ RSpec.describe Gitlab::Issuable::Clone::CopyResourceEventsService, feature_categ
|
|||
let_it_be(:project2) { create(:project, :public, group: group) }
|
||||
let_it_be(:new_issue) { create(:issue, project: project2) }
|
||||
let_it_be_with_reload(:original_issue) { create(:issue, project: project1) }
|
||||
let(:used_new_issue) { new_issue }
|
||||
|
||||
subject { described_class.new(user, original_issue, new_issue) }
|
||||
subject { described_class.new(user, original_issue, used_new_issue) }
|
||||
|
||||
it 'copies the resource label events' do
|
||||
resource_label_events = create_list(:resource_label_event, 2, issue: original_issue)
|
||||
|
@ -102,14 +103,51 @@ RSpec.describe Gitlab::Issuable::Clone::CopyResourceEventsService, feature_categ
|
|||
state_events = new_issue.reload.resource_state_events
|
||||
expect(state_events.size).to eq(3)
|
||||
|
||||
expect_state_event(state_events.first, issue: new_issue, state: 'opened')
|
||||
expect_state_event(state_events.second, issue: new_issue, state: 'closed')
|
||||
expect_state_event(state_events.third, issue: new_issue, state: 'reopened')
|
||||
expect_state_event(state_events.first, issue: new_issue, state: 'opened', namespace_id: new_issue.namespace_id)
|
||||
expect_state_event(state_events.second, issue: new_issue, state: 'closed', namespace_id: new_issue.namespace_id)
|
||||
expect_state_event(state_events.third, issue: new_issue, state: 'reopened', namespace_id: new_issue.namespace_id)
|
||||
end
|
||||
|
||||
context 'when new entity is a work item', :aggregate_failures do
|
||||
let(:used_new_issue) { new_issue.becomes(::WorkItem) } # rubocop:disable Cop/AvoidBecomes -- Less expensive than creating a new entity
|
||||
|
||||
it 'copies existing state events as expected' do
|
||||
subject.execute
|
||||
|
||||
state_events = used_new_issue.reload.resource_state_events
|
||||
expect(state_events.size).to eq(3)
|
||||
expect(state_events.pluck(:namespace_id)).to all(eq(project2.project_namespace_id))
|
||||
end
|
||||
|
||||
context 'when it is a group level work item' do
|
||||
let(:used_new_issue) { create(:work_item, :group_level, namespace: group) }
|
||||
|
||||
it 'copies existing state events as expected' do
|
||||
subject.execute
|
||||
|
||||
state_events = used_new_issue.reload.resource_state_events
|
||||
expect(state_events.size).to eq(3)
|
||||
expect(state_events.pluck(:namespace_id)).to all(eq(group.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the new entity is not of a supported type' do
|
||||
let(:used_new_issue) { create(:merge_request, source_project: project2) }
|
||||
|
||||
# No reason not to support merge requests other than it's not implemente yet. Should be fine to implement
|
||||
# if necessary, in the future
|
||||
it 'raises an unsupported type error' do
|
||||
expect do
|
||||
subject.execute
|
||||
end.to raise_error(StandardError, 'Copying resource events for MergeRequest is not supported yet')
|
||||
end
|
||||
end
|
||||
|
||||
def expect_state_event(event, expected_attrs)
|
||||
expect(event.issue_id).to eq(expected_attrs[:issue]&.id)
|
||||
expect(event.state).to eq(expected_attrs[:state])
|
||||
expect(event.namespace_id).to eq(expected_attrs[:namespace_id])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,11 +49,19 @@ RSpec.describe Discussions::ResolveService, feature_category: :code_review_workf
|
|||
end
|
||||
|
||||
context 'when not all discussions are resolved' do
|
||||
let(:other_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
|
||||
before do
|
||||
create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion
|
||||
end
|
||||
|
||||
it 'does not publish the discussions resolved event when the project requires all discussions to be resolved' do
|
||||
project.update!(only_allow_merge_if_all_discussions_are_resolved: true)
|
||||
|
||||
it 'does not publish the discussions resolved event' do
|
||||
expect { service.execute }.not_to publish_event(MergeRequests::DiscussionsResolvedEvent)
|
||||
end
|
||||
|
||||
it 'publishes the discussions resolved event when the project does not require all discussions to be resolved' do
|
||||
expect { service.execute }.to publish_event(MergeRequests::DiscussionsResolvedEvent)
|
||||
end
|
||||
end
|
||||
|
||||
it 'sends GraphQL triggers' do
|
||||
|
|
|
@ -79,32 +79,15 @@ RSpec.describe WebHooks::LogExecutionService, feature_category: :webhooks do
|
|||
end
|
||||
|
||||
context 'when a lease cannot be obtained' do
|
||||
where(:response_category, :executable, :needs_updating) do
|
||||
:ok | true | false
|
||||
:ok | false | true
|
||||
:failed | true | true
|
||||
:failed | false | false
|
||||
:error | true | true
|
||||
:error | false | false
|
||||
before do
|
||||
stub_exclusive_lease_taken(lease_key)
|
||||
end
|
||||
|
||||
with_them do
|
||||
subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }
|
||||
it 'creates the WebHookLog and skips hook state update' do
|
||||
expect(project_hook).not_to receive(:backoff!)
|
||||
expect(project_hook).not_to receive(:parent)
|
||||
|
||||
before do
|
||||
# stub LOCK_RETRY to be 0 in order for tests to run quicker
|
||||
stub_const("#{described_class.name}::LOCK_RETRY", 0)
|
||||
stub_exclusive_lease_taken(lease_key, timeout: described_class::LOCK_TTL)
|
||||
allow(project_hook).to receive(:executable?).and_return(executable)
|
||||
end
|
||||
|
||||
it 'raises an error if the hook needs to be updated' do
|
||||
if needs_updating
|
||||
expect { service.execute }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
|
||||
else
|
||||
expect { service.execute }.not_to raise_error
|
||||
end
|
||||
end
|
||||
expect { service.execute }.to change { ::WebHookLog.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -143,6 +126,20 @@ RSpec.describe WebHooks::LogExecutionService, feature_category: :webhooks do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when an unexpected error occurs while updating hook status' do
|
||||
let(:standard_error) { StandardError.new('Unexpected error') }
|
||||
|
||||
before do
|
||||
allow(project_hook).to receive(:enable!).and_raise(standard_error)
|
||||
end
|
||||
|
||||
it 'creates the WebHookLog and tracks the exception without raising an error' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(standard_error, hook_id: project_hook.id)
|
||||
|
||||
expect { service.execute }.to change { ::WebHookLog.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with url_variables' do
|
||||
before do
|
||||
project_hook.update!(
|
||||
|
|
|
@ -72,6 +72,7 @@ module RSpec
|
|||
private
|
||||
|
||||
def match_data?(actual, expected)
|
||||
return true if expected.nil?
|
||||
return true if actual.blank? && expected.blank?
|
||||
return false if actual.blank? || expected.blank?
|
||||
|
||||
|
|
|
@ -74,16 +74,6 @@ RSpec.describe 'event store matchers', feature_category: :shared do
|
|||
end
|
||||
|
||||
it 'validates the event data' do
|
||||
missing_data = -> do
|
||||
expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
|
||||
.to publish_event(FakeEventType1)
|
||||
end
|
||||
|
||||
expect(&missing_data).to raise_error <<~MESSAGE
|
||||
expected FakeEventType1 with no data to be published, but only the following events were published:
|
||||
- FakeEventType1 with {"id"=>1}
|
||||
MESSAGE
|
||||
|
||||
different_data = -> do
|
||||
expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
|
||||
.to publish_event(FakeEventType1).with({ 'id' => 2 })
|
||||
|
@ -94,6 +84,32 @@ RSpec.describe 'event store matchers', feature_category: :shared do
|
|||
- FakeEventType1 with {"id"=>1}
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
it 'allows any data if .with() is not used to specify data' do
|
||||
unspecified_data = -> do
|
||||
expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
|
||||
.to publish_event(FakeEventType1)
|
||||
end
|
||||
|
||||
expect(&unspecified_data).not_to raise_error
|
||||
end
|
||||
|
||||
it 'supports not_to when the event is published' do
|
||||
matcher = -> do
|
||||
expect { publishing_event(FakeEventType1, { 'id' => 1 }) }
|
||||
.not_to publish_event(FakeEventType1)
|
||||
end
|
||||
|
||||
expect(&matcher).to raise_error('expected FakeEventType1 not to be published')
|
||||
end
|
||||
|
||||
it 'supports not_to when the event is not published' do
|
||||
matcher = -> do
|
||||
expect { 'do nothing' }.not_to publish_event(FakeEventType1)
|
||||
end
|
||||
|
||||
expect(&matcher).not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'not_publish_event' do
|
||||
|
|
|
@ -70,9 +70,9 @@ internal/upload/destination/objectstore/upload_strategy.go:29: internal/upload/d
|
|||
internal/upload/destination/objectstore/uploader.go:5:2: G501: Blocklisted import crypto/md5: weak cryptographic primitive (gosec)
|
||||
internal/upload/destination/objectstore/uploader.go:95:12: G401: Use of weak cryptographic primitive (gosec)
|
||||
internal/upload/exif/exif.go:103:10: G204: Subprocess launched with variable (gosec)
|
||||
internal/upstream/routes.go:185:74: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
|
||||
internal/upstream/routes.go:245: Function 'configureRoutes' is too long (349 > 60) (funlen)
|
||||
internal/upstream/routes.go:510: internal/upstream/routes.go:510: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
|
||||
internal/upstream/routes.go:205:74: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
|
||||
internal/upstream/routes.go:265: Function 'configureRoutes' is too long (349 > 60) (funlen)
|
||||
internal/upstream/routes.go:530: internal/upstream/routes.go:530: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
|
||||
internal/upstream/upstream.go:116: internal/upstream/upstream.go:116: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: move to LabKit https://gitlab.com/..." (godox)
|
||||
internal/zipartifacts/metadata.go:118:54: G115: integer overflow conversion int -> uint32 (gosec)
|
||||
internal/zipartifacts/open_archive.go:74:28: response body must be closed (bodyclose)
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
// Package bodylimit provides HTTP request body size limiting functionality
|
||||
// for RoundTrippers, supporting different enforcement modes including
|
||||
// logging and strict enforcement with configurable size limits.
|
||||
package bodylimit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Mode defines the behavior of the request body middleware
|
||||
type Mode int
|
||||
|
||||
// contextKey to hold a body limit per request
|
||||
type contextKey string
|
||||
|
||||
// BodyLimitKey is the context key used to store the request body size limit
|
||||
const BodyLimitKey contextKey = "bodyLimit"
|
||||
|
||||
const (
|
||||
// ModeDisabled - no body size checking
|
||||
ModeDisabled Mode = iota
|
||||
// ModeLogging - log when body size exceeds limit but allow request to continue
|
||||
ModeLogging
|
||||
// ModeEnforced - reject requests that exceed body size limit
|
||||
ModeEnforced
|
||||
)
|
||||
|
||||
// RequestBodyTooLargeError is returned when a request body exceeds the configured size limit.
|
||||
type RequestBodyTooLargeError struct {
|
||||
Limit int64
|
||||
Read int64
|
||||
}
|
||||
|
||||
func (e *RequestBodyTooLargeError) Error() string {
|
||||
return fmt.Sprintf("request body too large: read %d bytes, limit %d bytes", e.Read, e.Limit)
|
||||
}
|
||||
|
||||
// countingReadCloser wraps an io.ReadCloser to count bytes read
|
||||
type countingReadCloser struct {
|
||||
reader io.ReadCloser
|
||||
count int64
|
||||
limit int64
|
||||
mode Mode
|
||||
}
|
||||
|
||||
func (crc *countingReadCloser) Read(p []byte) (n int, err error) {
|
||||
n, err = crc.reader.Read(p)
|
||||
newCount := atomic.AddInt64(&crc.count, int64(n))
|
||||
|
||||
if crc.mode == ModeEnforced && newCount > crc.limit {
|
||||
return n, &RequestBodyTooLargeError{Limit: crc.limit, Read: newCount}
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (crc *countingReadCloser) Close() error {
|
||||
return crc.reader.Close()
|
||||
}
|
||||
|
||||
func (crc *countingReadCloser) Count() int64 {
|
||||
return atomic.LoadInt64(&crc.count)
|
||||
}
|
||||
|
||||
type roundTripper struct {
|
||||
next http.RoundTripper
|
||||
mode Mode
|
||||
}
|
||||
|
||||
// NewRoundTripper creates a RoundTripper that logs or blocks requests
|
||||
// with request body size over the specified limit.
|
||||
func NewRoundTripper(next http.RoundTripper, mode Mode) http.RoundTripper {
|
||||
return &roundTripper{next: next, mode: mode}
|
||||
}
|
||||
|
||||
func (t *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// If disabled, just pass through
|
||||
if t.mode == ModeDisabled {
|
||||
return t.next.RoundTrip(r)
|
||||
}
|
||||
|
||||
// Extract limit from the provided context
|
||||
bodyLimit, ok := r.Context().Value(BodyLimitKey).(int64)
|
||||
if !ok || bodyLimit < 1 {
|
||||
return t.next.RoundTrip(r)
|
||||
}
|
||||
|
||||
// Handle empty request body case
|
||||
if r.Body == nil {
|
||||
return t.next.RoundTrip(r)
|
||||
}
|
||||
|
||||
// Use a custom reader to count
|
||||
bytesCounter := &countingReadCloser{reader: r.Body, count: 0, limit: bodyLimit, mode: t.mode}
|
||||
r.Body = bytesCounter
|
||||
|
||||
res, err := t.next.RoundTrip(r)
|
||||
|
||||
// Enforced mode processing
|
||||
if t.mode == ModeEnforced && err != nil {
|
||||
var bodyTooLargeErr *RequestBodyTooLargeError
|
||||
// Return 413 error code when request body is too large
|
||||
if errors.As(err, &bodyTooLargeErr) {
|
||||
return &http.Response{
|
||||
Status: http.StatusText(http.StatusRequestEntityTooLarge),
|
||||
StatusCode: http.StatusRequestEntityTooLarge,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
Body: io.NopCloser(strings.NewReader("Request Entity Too Large")),
|
||||
Request: r,
|
||||
Header: make(http.Header),
|
||||
Trailer: make(http.Header),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
|
@ -0,0 +1,544 @@
|
|||
package bodylimit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockRoundTripper implements http.RoundTripper for testing
|
||||
type mockRoundTripper struct {
|
||||
response *http.Response
|
||||
err error
|
||||
customHandler func(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func (m *mockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// Use custom handler if provided
|
||||
if m.customHandler != nil {
|
||||
return m.customHandler(r)
|
||||
}
|
||||
|
||||
return m.response, m.err
|
||||
}
|
||||
|
||||
func TestCountingReadCloser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
readSizes []int
|
||||
expectedCount int64
|
||||
mode Mode
|
||||
limit int64
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "single read within limit",
|
||||
data: "hello world",
|
||||
readSizes: []int{11},
|
||||
expectedCount: 11,
|
||||
mode: ModeEnforced,
|
||||
limit: 20,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multiple reads within limit",
|
||||
data: "hello world",
|
||||
readSizes: []int{5, 6},
|
||||
expectedCount: 11,
|
||||
mode: ModeEnforced,
|
||||
limit: 20,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "exceeds limit in enforced mode",
|
||||
data: "hello world this is too long",
|
||||
readSizes: []int{28},
|
||||
expectedCount: 28,
|
||||
mode: ModeEnforced,
|
||||
limit: 10,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "exceeds limit in logging mode",
|
||||
data: "hello world this is too long",
|
||||
readSizes: []int{28},
|
||||
expectedCount: 28,
|
||||
mode: ModeLogging,
|
||||
limit: 10,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty data",
|
||||
data: "",
|
||||
readSizes: []int{10},
|
||||
expectedCount: 0,
|
||||
mode: ModeEnforced,
|
||||
limit: 5,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := io.NopCloser(strings.NewReader(tt.data))
|
||||
counter := &countingReadCloser{
|
||||
reader: reader,
|
||||
count: 0,
|
||||
limit: tt.limit,
|
||||
mode: tt.mode,
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, size := range tt.readSizes {
|
||||
buf := make([]byte, size)
|
||||
_, err := counter.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
lastErr = err
|
||||
break // Stop reading after first error in enforced mode
|
||||
}
|
||||
}
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, lastErr, "expected error but got none")
|
||||
var bodyTooLargeErr *RequestBodyTooLargeError
|
||||
require.ErrorAs(t, lastErr, &bodyTooLargeErr, "error should be RequestBodyTooLargeError")
|
||||
require.Equal(t, tt.limit, bodyTooLargeErr.Limit, "error limit mismatch")
|
||||
} else {
|
||||
require.NoError(t, lastErr)
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectedCount, counter.Count(), "byte count mismatch")
|
||||
require.NoError(t, counter.Close())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripper_NoBodyLimit(t *testing.T) {
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeEnforced)
|
||||
|
||||
// Request without body limit in context
|
||||
req, err := http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("test data"))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestRoundTripper_NilBody(t *testing.T) {
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
customHandler: func(r *http.Request) (*http.Response, error) {
|
||||
// Try to read from the body - this would panic if nil body wasn't handled
|
||||
if r.Body != nil {
|
||||
buf := make([]byte, 10)
|
||||
_, err := r.Body.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
},
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeEnforced)
|
||||
|
||||
// Request with nil body (like GET requests)
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, int64(100))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", nil)
|
||||
require.NoError(t, err, "failed to create request")
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "status code mismatch")
|
||||
}
|
||||
|
||||
func TestRoundTripper_WithinLimit(t *testing.T) {
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
customHandler: func(r *http.Request) (*http.Response, error) {
|
||||
// Consume some of the body but stay within limit
|
||||
if r.Body != nil {
|
||||
buf := make([]byte, 5) // Read only 5 bytes
|
||||
r.Body.Read(buf)
|
||||
r.Body.Close()
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
},
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeEnforced)
|
||||
|
||||
// Request with body limit in context
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, int64(100))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", strings.NewReader("small data"))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestRoundTripper_ExceedsLimit_Logging(t *testing.T) {
|
||||
largeData := strings.Repeat("a", 150) // Exceed 100 byte limit
|
||||
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
customHandler: func(r *http.Request) (*http.Response, error) {
|
||||
// Consume the entire body to trigger counting
|
||||
if r.Body != nil {
|
||||
io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
},
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeLogging)
|
||||
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, int64(100))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", strings.NewReader(largeData))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
// In logging mode, request should still succeed
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestRoundTripper_ExceedsLimit_Enforced(t *testing.T) {
|
||||
largeData := strings.Repeat("a", 150) // Exceed 100 byte limit
|
||||
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
customHandler: func(r *http.Request) (*http.Response, error) {
|
||||
// Try to consume the entire body - this will trigger the limit error
|
||||
if r.Body != nil {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err // Return the error from counting reader
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
},
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeEnforced)
|
||||
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, int64(100))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", strings.NewReader(largeData))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
// In enforced mode, should return 413 error
|
||||
require.Equal(t, http.StatusRequestEntityTooLarge, res.StatusCode)
|
||||
|
||||
expectedStatus := http.StatusText(http.StatusRequestEntityTooLarge)
|
||||
require.Equal(t, expectedStatus, res.Status, "status text mismatch")
|
||||
|
||||
// Verify response body
|
||||
require.NotNil(t, res.Body, "response body is nil")
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedBody := "Request Entity Too Large"
|
||||
require.Equal(t, expectedBody, string(body), "response body mismatch")
|
||||
|
||||
// Verify request details are preserved
|
||||
require.Equal(t, req, res.Request, "request not preserved in response")
|
||||
}
|
||||
|
||||
func TestRoundTripper_BodyLimitEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
bodyLimit interface{}
|
||||
expectPass bool
|
||||
consumeBody bool
|
||||
}{
|
||||
{
|
||||
name: "zero limit",
|
||||
bodyLimit: int64(0),
|
||||
expectPass: true, // Zero limit should pass through (< 1 check)
|
||||
consumeBody: false,
|
||||
},
|
||||
{
|
||||
name: "negative limit",
|
||||
bodyLimit: int64(-1),
|
||||
expectPass: true, // Negative limit should pass through
|
||||
consumeBody: false,
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
bodyLimit: "100",
|
||||
expectPass: true, // Wrong type should pass through
|
||||
consumeBody: false,
|
||||
},
|
||||
{
|
||||
name: "valid limit exceeded",
|
||||
bodyLimit: int64(100),
|
||||
expectPass: false, // Valid limit with large data should not pass
|
||||
consumeBody: true, // Need to consume body for test to work
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
}
|
||||
|
||||
if tt.consumeBody {
|
||||
// Set custom handler to consume the body
|
||||
mockNext.customHandler = func(r *http.Request) (*http.Response, error) {
|
||||
if r.Body != nil {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err // Propagate the error
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
}
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeEnforced)
|
||||
|
||||
largeData := strings.Repeat("a", 150)
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, tt.bodyLimit)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", strings.NewReader(largeData))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
if tt.expectPass {
|
||||
require.Equal(t, http.StatusOK, res.StatusCode, "expected request to pass with status 200")
|
||||
} else {
|
||||
require.Equal(t, http.StatusRequestEntityTooLarge, res.StatusCode, "expected request to fail with status 413")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripper_NextRoundTripperError(t *testing.T) {
|
||||
expectedErr := io.EOF
|
||||
mockNext := &mockRoundTripper{
|
||||
response: nil,
|
||||
err: expectedErr,
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, ModeEnforced)
|
||||
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, int64(100))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", strings.NewReader("test"))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
require.ErrorIs(t, err, expectedErr, "expected EOF error")
|
||||
require.Nil(t, res, "expected nil response")
|
||||
}
|
||||
|
||||
func TestRequestBodyTooLargeError(t *testing.T) {
|
||||
err := &RequestBodyTooLargeError{
|
||||
Limit: 100,
|
||||
Read: 150,
|
||||
}
|
||||
|
||||
expected := "request body too large: read 150 bytes, limit 100 bytes"
|
||||
require.Equal(t, expected, err.Error(), "error message mismatch")
|
||||
}
|
||||
|
||||
func TestRoundTripper_DifferentModes(t *testing.T) {
|
||||
largeData := strings.Repeat("a", 150) // Exceed 100 byte limit
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
expectStatus int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "disabled mode bypasses limits",
|
||||
mode: ModeDisabled,
|
||||
expectStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "logging mode allows request",
|
||||
mode: ModeLogging,
|
||||
expectStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "enforced mode blocks request",
|
||||
mode: ModeEnforced,
|
||||
expectStatus: http.StatusRequestEntityTooLarge,
|
||||
expectError: false, // No error returned, just 413 response
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockNext := &mockRoundTripper{
|
||||
response: &http.Response{StatusCode: http.StatusOK},
|
||||
customHandler: func(r *http.Request) (*http.Response, error) {
|
||||
if r.Body != nil {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
},
|
||||
}
|
||||
|
||||
rt := NewRoundTripper(mockNext, tt.mode)
|
||||
|
||||
ctx := context.WithValue(context.Background(), BodyLimitKey, int64(100))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", strings.NewReader(largeData))
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := rt.RoundTrip(req)
|
||||
defer closeResponseBody(res)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err, "expected error but got none")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.Equal(t, tt.expectStatus, res.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegration tests the roundtripper with a real HTTP server
|
||||
func TestIntegration(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("success"))
|
||||
})
|
||||
|
||||
// Create test server
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
bodySize int
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "under limit with enforced mode",
|
||||
mode: ModeEnforced,
|
||||
bodySize: 90,
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "over limit with enforced mode",
|
||||
mode: ModeEnforced,
|
||||
bodySize: 150,
|
||||
expectedCode: http.StatusRequestEntityTooLarge,
|
||||
},
|
||||
{
|
||||
name: "over limit with logging mode",
|
||||
mode: ModeLogging,
|
||||
bodySize: 150,
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "over limit with disabled mode",
|
||||
mode: ModeDisabled,
|
||||
bodySize: 150,
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create HTTP client with our custom roundtripper
|
||||
transport := &contextSettingTransport{
|
||||
next: http.DefaultTransport,
|
||||
bodyLimit: 100,
|
||||
mode: tt.mode,
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// Make request
|
||||
body := strings.NewReader(strings.Repeat("a", tt.bodySize))
|
||||
resp, err := client.Post(server.URL, "text/plain", body)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedCode, resp.StatusCode)
|
||||
|
||||
defer closeResponseBody(resp)
|
||||
|
||||
// Verify response body for successful requests
|
||||
if tt.expectedCode == http.StatusOK {
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "success", string(respBody), "response body mismatch")
|
||||
} else if tt.expectedCode == http.StatusRequestEntityTooLarge {
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Request Entity Too Large", string(respBody), "error response body mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// contextSettingTransport wraps the request body roundtripper and sets context
|
||||
type contextSettingTransport struct {
|
||||
next http.RoundTripper
|
||||
bodyLimit int64
|
||||
mode Mode
|
||||
}
|
||||
|
||||
func (t *contextSettingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// Set body limit in context
|
||||
ctx := context.WithValue(r.Context(), BodyLimitKey, t.bodyLimit)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// Use our roundtripper
|
||||
rt := NewRoundTripper(t.next, t.mode)
|
||||
return rt.RoundTrip(r)
|
||||
}
|
||||
|
||||
func closeResponseBody(res *http.Response) {
|
||||
if res != nil && res.Body != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
}
|
|
@ -13,12 +13,15 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/gitlab-org/labkit/correlation"
|
||||
"gitlab.com/gitlab-org/labkit/tracing"
|
||||
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/badgateway"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/bodylimit"
|
||||
)
|
||||
|
||||
func mustParseAddress(address, scheme string) string {
|
||||
|
@ -73,7 +76,7 @@ func newBackendRoundTripper(backend *url.URL, socket string, proxyHeadersTimeout
|
|||
|
||||
return tracing.NewRoundTripper(
|
||||
correlation.NewInstrumentedRoundTripper(
|
||||
badgateway.NewRoundTripper(developmentMode, transport),
|
||||
badgateway.NewRoundTripper(developmentMode, bodylimit.NewRoundTripper(transport, bodyLimitMode)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -82,3 +85,20 @@ func newBackendRoundTripper(backend *url.URL, socket string, proxyHeadersTimeout
|
|||
func NewTestBackendRoundTripper(backend *url.URL) http.RoundTripper {
|
||||
return NewBackendRoundTripper(backend, "", 0, true)
|
||||
}
|
||||
|
||||
var bodyLimitMode = getBodyLimitMode()
|
||||
|
||||
func getBodyLimitMode() bodylimit.Mode {
|
||||
modeStr := strings.ToUpper(os.Getenv("WORKHORSE_REQUEST_LIMIT"))
|
||||
|
||||
switch modeStr {
|
||||
case "DISABLED":
|
||||
return bodylimit.ModeDisabled
|
||||
case "LOGGING":
|
||||
return bodylimit.ModeLogging
|
||||
case "ENFORCED":
|
||||
return bodylimit.ModeEnforced
|
||||
default:
|
||||
return bodylimit.ModeDisabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package upstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/ai_assist/duoworkflow"
|
||||
apipkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/artifacts"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/bodylimit"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/builds"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/channel"
|
||||
"gitlab.com/gitlab-org/gitlab/workhorse/internal/circuitbreaker"
|
||||
|
@ -56,6 +58,7 @@ type routeOptions struct {
|
|||
isGeoProxyRoute bool
|
||||
matchers []matcherFunc
|
||||
allowOrigins *regexp.Regexp
|
||||
bodyLimit int64
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -121,17 +124,29 @@ func withAllowOrigins(pattern string) func(*routeOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
func withBodyLimit(bodyLimit int64) func(*routeOptions) {
|
||||
return func(options *routeOptions) {
|
||||
options.bodyLimit = bodyLimit
|
||||
}
|
||||
}
|
||||
|
||||
func (u *upstream) observabilityMiddlewares(handler http.Handler, method string, metadata routeMetadata, opts *routeOptions) http.Handler {
|
||||
handler = log.AccessLogger(
|
||||
handler,
|
||||
log.WithAccessLogger(u.accessLogger),
|
||||
log.WithTrustedProxies(u.TrustedCIDRsForXForwardedFor),
|
||||
log.WithExtraFields(func(_ *http.Request) log.Fields {
|
||||
return log.Fields{
|
||||
fields := log.Fields{
|
||||
"route": metadata.regexpStr, // This field matches the `route` label in Prometheus metrics
|
||||
"route_id": metadata.routeID,
|
||||
"backend_id": metadata.backendID,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
fields["body_limit"] = opts.bodyLimit
|
||||
}
|
||||
|
||||
return fields
|
||||
}),
|
||||
)
|
||||
|
||||
|
@ -157,7 +172,8 @@ func (u *upstream) observabilityMiddlewares(handler http.Handler, method string,
|
|||
func (u *upstream) route(method string, metadata routeMetadata, handler http.Handler, opts ...func(*routeOptions)) routeEntry {
|
||||
// Instantiate a route with the defaults
|
||||
options := routeOptions{
|
||||
tracing: true,
|
||||
tracing: true,
|
||||
bodyLimit: 100 * 1024 * 1024, // 100MB
|
||||
}
|
||||
|
||||
for _, f := range opts {
|
||||
|
@ -174,6 +190,10 @@ func (u *upstream) route(method string, metadata routeMetadata, handler http.Han
|
|||
handler = corsMiddleware(handler, options.allowOrigins)
|
||||
}
|
||||
|
||||
if options.bodyLimit > 0 {
|
||||
handler = withBodyLimitContext(options.bodyLimit, handler)
|
||||
}
|
||||
|
||||
return routeEntry{
|
||||
method: method,
|
||||
regex: compileRegexp(metadata.regexpStr),
|
||||
|
@ -399,7 +419,7 @@ func configureRoutes(u *upstream) {
|
|||
u.route("POST",
|
||||
newRoute(apiGroupPattern+`/wikis/attachments\z`, "api_groups_wikis_attachments", railsBackend), tempfileMultipartProxy),
|
||||
u.route("POST",
|
||||
newRoute(apiPattern+`graphql\z`, "api_graphql", railsBackend), tempfileMultipartProxy),
|
||||
newRoute(apiPattern+`graphql\z`, "api_graphql", railsBackend), tempfileMultipartProxy, withBodyLimit(20*1024*1024)), // 20 Mb
|
||||
u.route("POST",
|
||||
newRoute(apiTopicPattern, "api_topics", railsBackend), tempfileMultipartProxy),
|
||||
u.route("PUT",
|
||||
|
@ -631,3 +651,12 @@ func allowedProxy(proxy http.Handler, dependencyProxyInjector *dependencyproxy.I
|
|||
|
||||
return proxy
|
||||
}
|
||||
|
||||
// Define a context key with a body limit value for route
|
||||
func withBodyLimitContext(bodyLimit int64, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), bodylimit.BodyLimitKey, bodyLimit)
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -1464,14 +1464,14 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.138.0.tgz#5db6d76ceedcf3716e9ce624b272a58052d8d121"
|
||||
integrity sha512-Jzd7GhmKxsQdCTttOe6C4AjqGvq8L91N6uUYnAmwnLGeY3aRD12BKBSgId5FrTH6rvk2w36o1+AwIqP+YuHV4g==
|
||||
|
||||
"@gitlab/ui@114.8.1":
|
||||
version "114.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-114.8.1.tgz#61edd78c7d4f7a0935efcbb5e916adbc44523412"
|
||||
integrity sha512-sKFl0Ud15vQEMv8ZBsUnyzsk4Lg17qjxYSCPvSUNjBsLfesAxK4JdJNy8X3gulKxXY3n793gtaqbbeIY6Ixmmw==
|
||||
"@gitlab/ui@115.0.1":
|
||||
version "115.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-115.0.1.tgz#a449a00b1b9352952d542a456cca3a210a02c706"
|
||||
integrity sha512-gZU8w2W1N36tqDjzzTsH1Mg3xjyVU+ki+2J3bHvw65ovQVF/+A7qRrn2CT07IV0L70zCNOP8RO9VKJKINxpp6A==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.7.1"
|
||||
echarts "^5.6.0"
|
||||
iframe-resizer "^4.3.2"
|
||||
iframe-resizer "^4.4.5"
|
||||
lodash "^4.17.21"
|
||||
popper.js "^1.16.1"
|
||||
portal-vue "^2.1.7"
|
||||
|
@ -8899,10 +8899,10 @@ iferr@^0.1.5:
|
|||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
|
||||
|
||||
iframe-resizer@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-4.3.2.tgz#42dd88345d18b9e377b6044dddb98c664ab0ce6b"
|
||||
integrity sha512-gOWo2hmdPjMQsQ+zTKbses08mDfDEMh4NneGQNP4qwePYujY1lguqP6gnbeJkf154gojWlBhIltlgnMfYjGHWA==
|
||||
iframe-resizer@^4.3.2, iframe-resizer@^4.4.5:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-4.4.5.tgz#f5048636e7f2fb5d9a09cc2ae78eb2da55ad555c"
|
||||
integrity sha512-U8bCywf/Gh07O69RXo6dXAzTtODQrxaHGHRI7Nt4ipXsuq6EMxVsOP/jjaP43YtXz/ibESS0uSVDN3sOGCzSmw==
|
||||
|
||||
ignore-by-default@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
|
Loading…
Reference in New Issue