Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-07-09 18:07:42 +00:00
parent b2bf7e325a
commit cf83f0a235
71 changed files with 69287 additions and 190 deletions

View File

@ -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"'

View File

@ -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)

View File

@ -1 +1 @@
14.42.0
14.43.0

View File

@ -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"},

View File

@ -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)

View File

@ -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"},

View File

@ -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)

View File

@ -4,10 +4,12 @@
"ContainerProtectionTagRule"
],
"AiCatalogItem": [
"AiCatalogAgent"
"AiCatalogAgent",
"AiCatalogFlow"
],
"AiCatalogItemVersion": [
"AiCatalogAgentVersion"
"AiCatalogAgentVersion",
"AiCatalogFlowVersion"
],
"AlertManagementIntegration": [
"AlertManagementHttpIntegration",

View File

@ -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(() => {

View File

@ -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>

View File

@ -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.'),

View File

@ -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">

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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' },

View File

@ -71,4 +71,4 @@ class ResourceStateEvent < ResourceEvent
end
end
ResourceStateEvent.prepend_mod_with('ResourceStateEvent')
ResourceStateEvent.prepend_mod

View File

@ -46,3 +46,5 @@ module Members
end
end
end
Members::Projects::CreatorService.prepend_mod_with('Members::Projects::CreatorService')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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).

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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"
}
```

View File

@ -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

View File

@ -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. |

View File

@ -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"
}
```

View File

@ -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).
![Grafana Dashboard for GitLab CI Pipelines Prometheus Exporter](img/ci_efficiency_pipeline_health_grafana_dashboard_v13_7.png)
![Grafana Dashboard showing CI run statuses and historical statistics including frequency and fail rate.](img/ci_efficiency_pipeline_health_grafana_dashboard_v13_7.png)
Alternatively, you can use a monitoring tool that can execute scripts, like
[`check_gitlab`](https://gitlab.com/6uellerBpanda/check_gitlab) for example.

View File

@ -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.
![Job variables in UI](img/trigger_variables_v11_6.png)
![A configuration panel for a CI trigger for token 4e19 showing UPLOAD_TO_CI set to true](img/trigger_variables_v11_6.png)
Using inputs to control pipeline behavior offers improved security and flexibility over CI/CD variables.

View File

@ -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;
```

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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 >}}

View File

@ -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.

View File

@ -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:

View File

@ -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 >}}

View File

@ -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

View File

@ -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 >}}

View File

@ -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**:
![A merge request that is blocked because a user requested changes](img/remove_v17_8.png)
@ -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**:
![A merge request that is blocked because a user requested changes](img/bypass_v17_2.png)
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**:
![This merge request contains a bypassed check, and should be merged with caution.](img/status_warning_v17_4.png)

View File

@ -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 >}}

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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 } });

View File

@ -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);
});
});
});

View File

@ -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.'}

View File

@ -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

View File

@ -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)

View File

@ -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' }

View File

@ -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

View File

@ -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

View File

@ -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!(

View File

@ -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?

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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)
})
}

View File

@ -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"