Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-07-03 09:11:34 +00:00
parent c4547be137
commit 960b02f579
30 changed files with 407 additions and 272 deletions

View File

@ -7,7 +7,7 @@ workflow:
include:
- local: .gitlab/ci/version.yml
- local: .gitlab/ci/global.gitlab-ci.yml
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@11.6.1"
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@11.7.0"
inputs:
job_name: "e2e-test-report"
job_stage: "report"
@ -17,7 +17,7 @@ include:
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
allure_job_name: "${QA_RUN_TYPE}"
- project: gitlab-org/quality/pipeline-common
ref: 11.6.1
ref: 11.7.0
file:
- /ci/notify-slack.gitlab-ci.yml
- /ci/qa-report.gitlab-ci.yml

View File

@ -1,9 +1,5 @@
import { initWebauthnAuthenticate, initWebauthnRegister } from './webauthn';
import { initWebauthnAuthenticate } from './webauthn';
export const mount2faAuthentication = () => {
initWebauthnAuthenticate();
};
export const mount2faRegistration = () => {
initWebauthnRegister();
};

View File

@ -1,6 +1,5 @@
import $ from 'jquery';
import WebAuthnAuthenticate from './authenticate';
import WebAuthnRegister from './register';
export const initWebauthnAuthenticate = () => {
if (!gon.webauthn) {
@ -16,14 +15,3 @@ export const initWebauthnAuthenticate = () => {
);
webauthnAuthenticate.start();
};
export const initWebauthnRegister = () => {
const el = $('#js-register-token-2fa');
if (!el.length) {
return;
}
const webauthnRegister = new WebAuthnRegister(el, gon.webauthn);
webauthnRegister.start();
};

View File

@ -1,79 +0,0 @@
import { __ } from '~/locale';
import WebAuthnError from './error';
import WebAuthnFlow from './flow';
import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util';
import { WEBAUTHN_REGISTER } from './constants';
// Register WebAuthn devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
export default class WebAuthnRegister {
constructor(container, webauthnParams) {
this.container = container;
this.renderInProgress = this.renderInProgress.bind(this);
this.webauthnOptions = convertCreateParams(webauthnParams.options);
this.flow = new WebAuthnFlow(container, {
message: '#js-register-2fa-message',
setup: '#js-register-token-2fa-setup',
error: '#js-register-token-2fa-error',
registered: '#js-register-token-2fa-registered',
});
this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress);
}
start() {
if (!supported()) {
// we show a special error message when the user visits the site
// using a non-ssl connection as this makes WebAuthn unavailable in
// any case, regardless of the used browser
this.renderNotSupported(!isHTTPS());
} else {
this.renderSetup();
}
}
register() {
navigator.credentials
.create({
publicKey: this.webauthnOptions,
})
.then((cred) => this.renderRegistered(JSON.stringify(convertCreateResponse(cred))))
.catch((err) => this.flow.renderError(new WebAuthnError(err, WEBAUTHN_REGISTER)));
}
renderSetup() {
this.flow.renderTemplate('setup');
this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress);
}
renderInProgress() {
this.flow.renderTemplate('message', {
message: __(
'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.',
),
});
return this.register();
}
renderRegistered(deviceResponse) {
this.flow.renderTemplate('registered');
// Prefer to do this instead of interpolating using Underscore templates
// because of JSON escaping issues.
this.container.find('#js-device-response').val(deviceResponse);
}
renderNotSupported(noHttps) {
const message = noHttps
? __(
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
)
: __(
"Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).",
);
this.flow.renderTemplate('message', { message });
}
}

View File

@ -11,11 +11,12 @@ import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { __, sprintf, s__ } from '~/locale';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { splitCamelCase } from '~/lib/utils/text_utility';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { useNotes } from '~/notes/store/legacy_notes';
import Tracking from '~/tracking';
import ReplyButton from './note_actions/reply_button.vue';
import TimelineEventButton from './note_actions/timeline_event_button.vue';
@ -25,6 +26,7 @@ export default {
deleteCommentLabel: __('Delete comment'),
moreActionsLabel: __('More actions'),
reportAbuse: __('Report abuse'),
GENIE_CHAT_FEEDBACK_THANKS: s__('AI|Thanks for your feedback!'),
},
name: 'NoteActions',
components: {
@ -41,7 +43,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [resolvedStatusMixin],
mixins: [resolvedStatusMixin, Tracking.mixin()],
props: {
author: {
type: Object,
@ -65,6 +67,11 @@ export default {
required: false,
default: '',
},
isAmazonQCodeReview: {
type: Boolean,
required: false,
default: false,
},
isAuthor: {
type: Boolean,
required: false,
@ -143,6 +150,7 @@ export default {
data() {
return {
isReportAbuseDrawerOpen: false,
feedbackReceived: false,
};
},
computed: {
@ -206,6 +214,13 @@ export default {
}
return null;
},
feedbackModalComponent() {
// Only load the EE component if this is an Amazon Q code review
if (this.isAmazonQCodeReview) {
return () => import('ee_component/ai/components/duo_chat_feedback_modal.vue');
}
return null;
},
},
methods: {
...mapActions(useNotes, ['toggleAwardRequest', 'promoteCommentToTimelineEvent']),
@ -221,6 +236,28 @@ export default {
onAbuse() {
this.toggleReportAbuseDrawer(true);
},
showFeedbackModal() {
this.$refs.feedbackModal.show();
},
/**
* Tracks feedback submitted for Amazon Q code reviews
* @param {Object} options - The feedback options
* @param {Array<string>} [options.feedbackOptions] - Array of selected feedback options (e.g. ['helpful', 'incorrect'])
* @param {string} [options.extendedFeedback] - Additional text feedback provided by the user
*/
trackFeedback({ feedbackOptions, extendedFeedback } = {}) {
this.track('amazon_q_code_review_feedback', {
action: 'amazon_q',
label: 'code_review_feedback',
property: feedbackOptions,
extra: {
extendedFeedback,
note_id: this.noteId,
},
});
this.feedbackReceived = true;
},
onCopyUrl() {
this.$toast.show(__('Link copied to clipboard.'));
},
@ -361,18 +398,14 @@ export default {
:data-clipboard-text="noteUrl"
@action="onCopyUrl"
>
<template #list-item>
{{ __('Copy link') }}
</template>
<template #list-item> {{ __('Copy link') }} </template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
v-if="canAssign"
data-testid="assign-user"
@action="assignUser"
>
<template #list-item>
{{ displayAssignUserText }}
</template>
<template #list-item> {{ displayAssignUserText }} </template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-group v-if="canReportAsAbuse || canEdit" bordered>
<gl-disclosure-dropdown-item
@ -380,9 +413,14 @@ export default {
data-testid="report-abuse-button"
@action="onAbuse"
>
<template #list-item>
{{ $options.i18n.reportAbuse }}
</template>
<template #list-item> {{ $options.i18n.reportAbuse }} </template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
v-if="isAmazonQCodeReview && !feedbackReceived"
data-testid="amazon-q-feedback-button"
@action="showFeedbackModal"
>
<template #list-item> {{ s__('AmazonQ|Provide feedback on code review') }} </template>
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
v-if="canEdit"
@ -390,15 +428,19 @@ export default {
variant="danger"
@action="onDelete"
>
<template #list-item>
{{ __('Delete comment') }}
</template>
<template #list-item> {{ __('Delete comment') }} </template>
</gl-disclosure-dropdown-item>
</gl-disclosure-dropdown-group>
</gl-disclosure-dropdown>
</div>
<!-- IMPORTANT: show this component lazily because it causes layout thrashing -->
<!-- https://gitlab.com/gitlab-org/gitlab/-/issues/331172#note_1269378396 -->
<component
:is="feedbackModalComponent"
v-if="feedbackModalComponent && !feedbackReceived"
ref="feedbackModal"
@feedback-submitted="trackFeedback"
/>
<abuse-category-selector
v-if="canReportAsAbuse && isReportAbuseDrawerOpen"
:reported-user-id="authorId"

View File

@ -248,6 +248,9 @@ export default {
const isFileComment = this.note.position?.position_type === 'file';
return !this.isOverviewTab && (this.line || isFileComment);
},
isAmazonQCodeReview() {
return this.author.username === 'amazon-q';
},
},
created() {
const line = this.note.position?.line_range?.start || this.line;
@ -517,6 +520,7 @@ export default {
:is-resolved="note.resolved || note.resolve_discussion"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
:is-amazon-q-code-review="isAmazonQCodeReview"
:is-draft="note.isDraft"
:resolve-discussion="note.isDraft && note.resolve_discussion"
:discussion-id="discussionId"

View File

@ -1,8 +1,6 @@
import { mount2faRegistration } from '~/authentication/mount_2fa';
import { initWebAuthnRegistration } from '~/authentication/webauthn/registration';
import { initRecoveryCodes, initTwoFactorConfirm } from '~/authentication/two_factor_auth';
mount2faRegistration();
initWebAuthnRegistration();
initRecoveryCodes();
initTwoFactorConfirm();

View File

@ -43,24 +43,31 @@ export default {
getBaseURL() {
return getBaseURL();
},
filesCountSummary() {
return n__(
'GlobalSearch|Showing file %{filesFrom} from %{filesTo}',
'GlobalSearch|Showing %{filesFrom}%{filesTo} out of %{filesTotal} files',
this.allFilesResults,
);
},
resultsSimple() {
return n__(
'GlobalSearch|Showing 1 code result for %{term}',
'GlobalSearch|Showing %{resultsTotal} code results for %{term}',
'GlobalSearch|Showing 1 code result for %{term}.',
'GlobalSearch|Showing %{resultsTotal} code results for %{term}.',
this?.resultsTotal ?? 0,
);
},
statusGroup() {
return n__(
'GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}',
'GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}',
'GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}.',
'GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}.',
this?.resultsTotal ?? 0,
);
},
statusProject() {
return n__(
'GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}',
'GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}',
'GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}.',
'GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}.',
this?.resultsTotal ?? 0,
);
},
@ -74,43 +81,56 @@ export default {
</script>
<template>
<div class="search-results-status gl-my-4">
<gl-sprintf v-if="!query.project_id && !query.group_id" :message="resultsSimple">
<template #resultsTotal>{{ resultsTotal }}</template>
<template #term
><code>{{ query.search }}</code></template
>
</gl-sprintf>
<div class="gl-flex gl-flex-wrap gl-items-center gl-justify-between">
<div
class="search-results-status gl-my-4"
role="status"
:aria-label="s__(`GlobalSearch|Search results summary`)"
>
<gl-sprintf v-if="!query.project_id && !query.group_id" :message="resultsSimple">
<template #resultsTotal>{{ resultsTotal }}</template>
<template #term
><code>{{ query.search }}</code></template
>
</gl-sprintf>
<gl-sprintf v-if="!query.project_id && query.group_id" :message="statusGroup">
<template #resultsTotal>{{ resultsTotal }}</template>
<template #term
><code>{{ query.search }}</code></template
><template #groupNameLink
><gl-link :href="`${getBaseURL}/${groupInitialJson.full_path}`">{{
groupInitialJson.full_name
}}</gl-link></template
>
</gl-sprintf>
<gl-sprintf v-if="!query.project_id && query.group_id" :message="statusGroup">
<template #resultsTotal>{{ resultsTotal }}</template>
<template #term
><code>{{ query.search }}</code></template
><template #groupNameLink
><gl-link :href="`${getBaseURL}/${groupInitialJson.full_path}`">{{
groupInitialJson.full_name
}}</gl-link></template
>
</gl-sprintf>
<gl-sprintf v-if="query.project_id" :message="statusProject">
<template #resultsTotal>{{ resultsTotal }}</template>
<template #term
><code>{{ query.search }}</code></template
>
<template #branchDropdown>
<ref-selector
:project-id="String(projectInitialJson.id)"
:value="repositoryRef"
class="gl-inline-block"
@input="handleInput"
/>
</template>
<template #ProjectWithGroupPathLink
><gl-link :href="`${getBaseURL}/${projectInitialJson.full_path}`">{{
projectInitialJson.name_with_namespace
}}</gl-link></template
>
</gl-sprintf>
<gl-sprintf v-if="query.project_id" :message="statusProject">
<template #resultsTotal>{{ resultsTotal }}</template>
<template #term
><code>{{ query.search }}</code></template
>
<template #branchDropdown>
<ref-selector
:project-id="String(projectInitialJson.id)"
:value="repositoryRef"
class="gl-inline-block"
@input="handleInput"
/>
</template>
<template #ProjectWithGroupPathLink
><gl-link :href="`${getBaseURL}/${projectInitialJson.full_path}`">{{
projectInitialJson.name_with_namespace
}}</gl-link></template
>
</gl-sprintf>
</div>
<div class="gl-my-4" role="status" :aria-label="s__('GlobalSearch|Files count summary')">
<gl-sprintf :message="filesCountSummary">
<template #filesFrom>{{ showingFilesFrom }}</template>
<template #filesTo>{{ showingFilesTo }}</template>
<template #filesTotal>{{ allFilesResults }}</template>
</gl-sprintf>
</div>
</div>
</template>

View File

@ -268,6 +268,7 @@ export const useAccessTokens = defineStore('accessTokens', {
this.page = page;
this.showCreateForm = showCreateForm;
this.sorting = sorting;
this.token = null;
this.urlCreate = urlCreate;
this.urlRevoke = urlRevoke;
this.urlRotate = urlRotate;

View File

@ -322,3 +322,11 @@ $merge-request-sticky-header-height: 45px;
width: 12px;
height: 12px;
}
// Firefox rendering bug fix - force new stacking context
.diffs.tab-pane,
.notes.tab-pane {
@supports (-moz-appearance: none) {
transform: translateZ(0);
}
}

View File

@ -84,7 +84,7 @@ module Resolvers
end
def unconditional_includes
[:creator, :group, :invited_groups, :project_setting]
[:creator, :group, :invited_groups, :project_setting, :project_namespace]
end
def finder_params(args)

View File

@ -84,7 +84,8 @@ module Types
field :name, GraphQL::Types::String,
null: false,
description: 'Name of the project without the namespace.'
description: 'Name of the project without the namespace.',
scopes: [:api, :read_api, :ai_workflows]
field :name_with_namespace, GraphQL::Types::String,
null: false,
@ -92,7 +93,8 @@ module Types
field :description, GraphQL::Types::String,
null: true,
description: 'Short description of the project.'
description: 'Short description of the project.',
scopes: [:api, :read_api, :ai_workflows]
field :tag_list, GraphQL::Types::String,
null: true,
@ -107,15 +109,18 @@ module Types
field :http_url_to_repo, GraphQL::Types::String,
null: true,
description: 'URL to connect to the project via HTTPS.'
description: 'URL to connect to the project via HTTPS.',
scopes: [:api, :read_api, :ai_workflows]
field :ssh_url_to_repo, GraphQL::Types::String,
null: true,
description: 'URL to connect to the project via SSH.'
description: 'URL to connect to the project via SSH.',
scopes: [:api, :read_api, :ai_workflows]
field :web_url, GraphQL::Types::String,
null: true,
description: 'Web URL of the project.'
description: 'Web URL of the project.',
scopes: [:api, :read_api, :ai_workflows]
field :forks_count, GraphQL::Types::Int,
null: false,

View File

@ -701,6 +701,7 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="queryduoworkflowworkflowsprojectpath"></a>`projectPath` | [`ID`](#id) | Full path of the project containing the workflows. |
| <a id="queryduoworkflowworkflowssort"></a>`sort` | [`Sort`](#sort) | Sort workflows by the criteria. |
| <a id="queryduoworkflowworkflowstype"></a>`type` | [`String`](#string) | Type of workflow to filter by (e.g., software_development). |
| <a id="queryduoworkflowworkflowsworkflowid"></a>`workflowId` | [`AiDuoWorkflowsWorkflowID`](#aiduoworkflowsworkflowid) | Workflow ID to filter by. |
### `Query.echo`
@ -27179,12 +27180,20 @@ A Duo Workflow.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="duoworkflowagentprivilegesnames"></a>`agentPrivilegesNames` | [`[String!]`](#string) | Privileges granted to the agent during workflow execution. |
| <a id="duoworkflowallowagenttorequestuser"></a>`allowAgentToRequestUser` | [`Boolean`](#boolean) | Allow the agent to request user input. |
| <a id="duoworkflowcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the workflow was created. |
| <a id="duoworkflowenvironment"></a>`environment` | [`WorkflowEnvironment`](#workflowenvironment) | Environment, e.g., ide or web. |
| <a id="duoworkflowfirstcheckpoint"></a>`firstCheckpoint` | [`DuoWorkflowEvent`](#duoworkflowevent) | First checkpoint of the workflow. |
| <a id="duoworkflowgoal"></a>`goal` | [`String`](#string) | Goal of the workflow. |
| <a id="duoworkflowhumanstatus"></a>`humanStatus` | [`String!`](#string) | Human-readable status of the workflow. |
| <a id="duoworkflowid"></a>`id` | [`ID!`](#id) | ID of the workflow. |
| <a id="duoworkflowmcpenabled"></a>`mcpEnabled` | [`Boolean`](#boolean) | Has MCP been enabled for the namespace. |
| <a id="duoworkflowpreapprovedagentprivilegesnames"></a>`preApprovedAgentPrivilegesNames` | [`[String!]`](#string) | Privileges pre-approved for the agent during workflow execution. |
| <a id="duoworkflowproject"></a>`project` | [`Project!`](#project) | Project that the workflow is in. |
| <a id="duoworkflowprojectid"></a>`projectId` | [`ProjectID!`](#projectid) | ID of the project. |
| <a id="duoworkflowstatus"></a>`status` | [`DuoWorkflowStatus`](#duoworkflowstatus) | Status of the workflow. |
| <a id="duoworkflowstatusname"></a>`statusName` | [`String`](#string) | Status Name of the workflow. |
| <a id="duoworkflowupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the workflow was last updated. |
| <a id="duoworkflowuserid"></a>`userId` | [`UserID!`](#userid) | ID of the user. |
| <a id="duoworkflowworkflowdefinition"></a>`workflowDefinition` | [`String`](#string) | Duo Workflow type based on its capabilities. |

View File

@ -12,25 +12,21 @@ title: Service account users API
{{< /details >}}
Use this API to interact with service accounts. Service accounts are a specific type of user, and many attributes of a service account can also be managed through the
[Users API](users.md) by administrators on GitLab Self Self-Managed instances.
Use this API to interact with service accounts for an entire GitLab instance. For more information,
see [service accounts](../user/profile/service_accounts.md).
## List all service account users
Service accounts are a type of user, and you can also use the [users API](users.md) to manage service accounts.
{{< details >}}
- Tier: Premium, Ultimate
- Offering: GitLab Self-Managed, GitLab Dedicated
{{< /details >}}
## List all service accounts for an instance
{{< history >}}
- List all service account users [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416729) in GitLab 17.1.
- List all service accounts [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/416729) in GitLab 17.1.
{{< /history >}}
Lists all service account users.
Lists all service accounts associated with the GitLab instance. Does not list service accounts
associated with a specific group.
Use the `page` and `per_page` [pagination parameters](rest/_index.md#offset-based-pagination) to filter the results.
@ -45,9 +41,9 @@ GET /service_accounts
Supported attributes:
| Attribute | Type | Required | Description |
|:-----------|:-------|:---------|:------------|
| ---------- | ------ | -------- | ----------- |
| `order_by` | string | no | Attribute to order results by. Possible values: `id` or `username`. Default value: `id`. |
| `sort` | string | no | Direction to sort results by. Possible values: `desc` or `asc`. Default value: `desc`. |
| `sort` | string | no | Direction to sort results by. Possible values: `desc` or `asc`. Default value: `desc`. |
Example request:
@ -72,16 +68,17 @@ Example response:
]
```
## Create a service account user
## Create a service account for an instance
{{< history >}}
- Create a service account user was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/406782) in GitLab 16.1
- Username and name attributes [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144841) in GitLab 16.10.
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/406782) in GitLab 16.1
- `username` and `name` attributes [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144841) in GitLab 16.10.
- `email` attribute [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178689) in GitLab 17.9.
{{< /history >}}
Creates a service account user.
Creates a service account associated with the GitLab instance.
Prerequisites:
@ -89,15 +86,16 @@ Prerequisites:
```plaintext
POST /service_accounts
POST /service_accounts?email=custom_email@gitlab.example.com
```
Supported attributes:
| Attribute | Type | Required | Description |
|:-----------|:-------|:---------|:------------|
| ---------- | ------ | -------- | ----------- |
| `name` | string | no | Name of the user. If not set, uses `Service account user`. |
| `username` | string | no | Username of the user account. If not set, generates a name prepended with `service_account_`. |
| `email` | string | no | Email of the user account. If not set, generates a no-reply email address. |
| `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. |
Example request:
@ -116,38 +114,5 @@ Example response:
}
```
### Specify a custom email address
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/178689) in GitLab 17.9.
{{< /history >}}
You can specify a custom email address at service account creation to receive
notifications on this service account's actions.
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "email=custom_email@gitlab.example.com" "https://gitlab.example.com/api/v4/service_accounts"
```
Example response:
```json
{
"id": 57,
"username": "service_account_6018816a18e515214e0c34c2b33523fc",
"name": "Service account user",
"email": "custom_email@gitlab.example.com"
}
```
This fails if the email address has already been taken by another user:
```json
{
"message": "400 Bad request - Email has already been taken"
}
```
If the email address defined by the `email` attribute is already in use by another user,
returns a `400 Bad request` error.

View File

@ -4695,16 +4695,17 @@ relative to `refs/heads/branch1` and the pipeline source is a merge request even
- CI/CD variable support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/283881) in GitLab 15.6.
- Maximum number of checks against `exists` patterns or file paths [increased](https://gitlab.com/gitlab-org/gitlab/-/issues/227632) from 10,000 to 50,000 in GitLab 17.7.
- Support for directory paths [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327485) in GitLab 18.2.
{{< /history >}}
Use `exists` to run a job when certain files exist in the repository.
Use `exists` to run a job when certain files or directories exist in the repository.
**Keyword type**: Job keyword. You can use it as part of a job or an [`include`](#include).
**Supported values**:
- An array of file paths. Paths are relative to the project directory (`$CI_PROJECT_DIR`)
- An array of file or directory paths. Paths are relative to the project directory (`$CI_PROJECT_DIR`)
and can't directly link outside it. File paths can use glob patterns and
[CI/CD variables](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
@ -4747,13 +4748,14 @@ In this example:
- `exists` resolves to `true` if any of the listed files are found (an `OR` operation).
- With job-level `rules:exists`, GitLab searches for the files in the project and
ref that runs the pipeline. When using [`include` with `rules:exists`](includes.md#include-with-rulesexists),
GitLab searches for the files in the project and ref of the file that contains the `include`
GitLab searches for the files or directories in the project and ref of the file that contains the `include`
section. The project containing the `include` section can be different than the project
running the pipeline when using:
- [Nested includes](includes.md#use-nested-includes).
- [Compliance pipelines](../../user/compliance/compliance_pipelines.md).
- `rules:exists` cannot search for the presence of [artifacts](../jobs/job_artifacts.md),
because `rules` evaluation happens before jobs run and artifacts are fetched.
- To test the existence of a directory, the path must end with a forward slash (/)
##### `rules:exists:paths`

View File

@ -1991,6 +1991,8 @@ In the GitLab product help, a set of cards appears as an unordered list of links
Card descriptions are populated from the `description` metadata on the Markdown page headers.
Use cards on top-level pages where the cards are the only content on the page.
## Maintained versions
Use the maintained versions shortcode to create an unordered list of the currently

View File

@ -221,13 +221,13 @@ Tests can then be ran with the `FIPS` variable set:
You can run the test (or perform the test steps manually) against your local GitLab instance to see if the failure is reproducible. For example:
``` shell
WEBDRIVER_HEADLESS=false bundle exec bin/qa Test::Instance::All http://localhost:3000 qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
WEBDRIVER_HEADLESS=false bundle exec bin/qa Test::Instance::All http://localhost:3000 qa/specs/features/browser_ui/9_tenant_scale/project/create_project_spec.rb
```
Orchestrated tests are excluded by default. To run them, use `-- --tag orchestrated` before your file name. For example:
``` shell
WEBDRIVER_HEADLESS=false bundle exec bin/qa Test::Instance::All http://localhost:3000 -- --tag orchestrated qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
WEBDRIVER_HEADLESS=false bundle exec bin/qa Test::Instance::All http://localhost:3000 -- --tag orchestrated qa/specs/features/browser_ui/9_tenant_scale/project/create_project_spec.rb
```
### Run the test against a GitLab Docker container
@ -248,7 +248,7 @@ To run Nightly images change `registry.gitlab.com/gitlab-org/build/omnibus-gitla
You can now run the test against this Docker instance. E.g.:
``` shell
WEBDRIVER_HEADLESS=false bundle exec bin/qa Test::Instance::All http://localhost qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
WEBDRIVER_HEADLESS=false bundle exec bin/qa Test::Instance::All http://localhost qa/specs/features/browser_ui/9_tenant_scale/project/create_project_spec.rb
```
### Run the tests against CustomersDot staging environment

View File

@ -208,7 +208,7 @@ This token authenticates the service account token used to manage GitLab deploym
To create the service account token:
1. [Create a service account user](../../api/user_service_accounts.md#create-a-service-account-user).
1. [Create a service account user](../../api/user_service_accounts.md#create-a-service-account-for-an-instance).
1. [Add the service account to a group or project](../../api/members.md#add-a-member-to-a-group-or-project)
by using your personal access token.
1. [Add the service account to protected environments](../../ci/environments/protected_environments.md#protecting-environments).

View File

@ -29,7 +29,7 @@ Service accounts:
- Do not use a seat.
- Cannot sign in to GitLab through the UI.
- Are identified in the group and project membership as service accounts.
- Do not receive notification emails without [additional configuration](../../api/user_service_accounts.md#specify-a-custom-email-address).
- Do not receive notification emails without [adding a custom email address](../../api/user_service_accounts.md#create-a-service-account-for-an-instance).
- Are not [billable users](../../subscriptions/self_managed/_index.md#billable-users) or [internal users](../../administration/internal_users.md).
- Cannot be used with [trial versions](https://gitlab.com/-/trial_registrations/new?glm_source=docs.gitlab.com&glm_content=free-user-limit-faq/ee/user/free_user_limit.html) of GitLab.com.
- Can be used with trial versions of GitLab Self-Managed and GitLab Dedicated.

View File

@ -21,6 +21,10 @@ module ActiveContext
@executor = executor_klass.new(self)
end
def name
raise NotImplementedError
end
def client_klass
raise NotImplementedError
end

View File

@ -6,6 +6,10 @@ module ActiveContext
class Adapter
include ActiveContext::Databases::Concerns::Adapter
def name
'elasticsearch'
end
def client_klass
ActiveContext::Databases::Elasticsearch::Client
end

View File

@ -6,6 +6,10 @@ module ActiveContext
class Adapter
include ActiveContext::Databases::Concerns::Adapter
def name
'opensearch'
end
def client_klass
ActiveContext::Databases::Opensearch::Client
end

View File

@ -8,6 +8,10 @@ module ActiveContext
delegate :bulk_process, to: :client
def name
'postgresql'
end
def client_klass
ActiveContext::Databases::Postgresql::Client
end

View File

@ -70,7 +70,11 @@ module Gitlab
def exact_matches?(paths, exact_globs)
exact_globs.any? do |glob|
paths.bsearch { |path| glob <=> path }
if glob.end_with?("/")
paths.bsearch { |path| path.start_with?(glob) }
else
paths.bsearch { |path| glob <=> path }
end
end
end

View File

@ -2568,6 +2568,9 @@ msgstr ""
msgid "AI|Thank you for your feedback."
msgstr ""
msgid "AI|Thanks for your feedback!"
msgstr ""
msgid "AI|The container element wasn't found, stopping AI Genie."
msgstr ""
@ -6961,6 +6964,9 @@ msgstr ""
msgid "AmazonQ|On by default"
msgstr ""
msgid "AmazonQ|Provide feedback on code review"
msgstr ""
msgid "AmazonQ|Provider URL"
msgstr ""
@ -29243,6 +29249,9 @@ msgstr ""
msgid "GlobalSearch|Files"
msgstr ""
msgid "GlobalSearch|Files count summary"
msgstr ""
msgid "GlobalSearch|Filters"
msgstr ""
@ -29432,6 +29441,9 @@ msgstr ""
msgid "GlobalSearch|Search results are loading"
msgstr ""
msgid "GlobalSearch|Search results summary"
msgstr ""
msgid "GlobalSearch|Settings"
msgstr ""
@ -29446,18 +29458,23 @@ msgstr ""
msgid "GlobalSearch|Show more"
msgstr ""
msgid "GlobalSearch|Showing 1 code result for %{term}"
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term}"
msgid "GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}."
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}."
msgstr[0] ""
msgstr[1] ""
msgid "GlobalSearch|Showing 1 code result for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}"
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in %{branchDropdown} of %{ProjectWithGroupPathLink}"
msgid "GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}."
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}."
msgstr[0] ""
msgstr[1] ""
msgid "GlobalSearch|Showing 1 code result for %{term} in group %{groupNameLink}"
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term} in group %{groupNameLink}"
msgid "GlobalSearch|Showing 1 code result for %{term}."
msgid_plural "GlobalSearch|Showing %{resultsTotal} code results for %{term}."
msgstr[0] ""
msgstr[1] ""
msgid "GlobalSearch|Showing file %{filesFrom} from %{filesTo}"
msgid_plural "GlobalSearch|Showing %{filesFrom}%{filesTo} out of %{filesTotal} files"
msgstr[0] ""
msgstr[1] ""
@ -70752,6 +70769,9 @@ msgstr ""
msgid "WorkItem|Mark this item as related to: %{workItemType} %{workItemReference}"
msgstr ""
msgid "WorkItem|Maximum %{maxLimit} statuses reached. Remove a status to add more."
msgstr ""
msgid "WorkItem|Merged"
msgstr ""

View File

@ -37,6 +37,8 @@ describe('noteActions', () => {
const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-button"]`);
const findDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const findFeedbackButton = () => wrapper.find('[data-testid="amazon-q-feedback-button"]');
const findDeleteButton = () => wrapper.find('.js-note-delete');
const setupStoreForIncidentTimelineEvents = ({
userCanAdd,
@ -192,7 +194,7 @@ describe('noteActions', () => {
});
it('should be possible to delete comment', () => {
expect(wrapper.find('.js-note-delete').exists()).toBe(true);
expect(findDeleteButton().exists()).toBe(true);
});
it('should not be possible to assign or unassign the comment author in a merge request', () => {
@ -404,6 +406,109 @@ describe('noteActions', () => {
});
});
describe('Amazon Q code review feedback', () => {
beforeEach(() => {
useNotes().setUserData(userDataMock);
wrapper = mountNoteActions({
...props,
isAmazonQCodeReview: true,
canEdit: true,
canReportAsAbuse: true,
});
});
it('renders the feedback button when isAmazonQCodeReview is true', () => {
expect(findFeedbackButton().exists()).toBe(true);
expect(findFeedbackButton().text()).toBe('Provide feedback on code review');
});
it('renders the feedback modal when isAmazonQCodeReview is true and feedback not received', () => {
// The modal should be rendered when feedbackReceived is false
expect(wrapper.vm.feedbackReceived).toBe(false);
// Check that the modal component is conditionally rendered
const feedbackModal = wrapper.findComponent({ ref: 'feedbackModal' });
expect(feedbackModal.exists()).toBe(true);
});
it('hides the feedback modal after feedback is submitted', async () => {
// Initially modal should be visible
expect(wrapper.vm.feedbackReceived).toBe(false);
// Simulate feedback submission
const feedbackData = {
feedbackOptions: ['helpful'],
extendedFeedback: 'Great review!',
};
wrapper.vm.trackFeedback(feedbackData);
await nextTick();
// After feedback submission, feedbackReceived should be true
expect(wrapper.vm.feedbackReceived).toBe(true);
// Modal should no longer be rendered
const feedbackModal = wrapper.findComponent({ ref: 'feedbackModal' });
expect(feedbackModal.exists()).toBe(false);
});
it('hides the feedback button after feedback is received', async () => {
// Initially button should be visible
expect(findFeedbackButton().exists()).toBe(true);
// Set feedbackReceived to true
wrapper.vm.feedbackReceived = true;
await nextTick();
// Button should no longer be visible
expect(findFeedbackButton().exists()).toBe(false);
});
it('tracks feedback with correct parameters when submitted', () => {
const trackSpy = jest.spyOn(wrapper.vm, 'track').mockImplementation(() => {});
const feedbackData = {
feedbackOptions: ['helpful'],
extendedFeedback: 'Great review!',
};
wrapper.vm.trackFeedback(feedbackData);
expect(trackSpy).toHaveBeenCalledWith('amazon_q_code_review_feedback', {
action: 'amazon_q',
label: 'code_review_feedback',
property: feedbackData.feedbackOptions,
extra: {
extendedFeedback: feedbackData.extendedFeedback,
note_id: wrapper.vm.noteId,
},
});
// Verify that feedbackReceived is set to true after tracking
expect(wrapper.vm.feedbackReceived).toBe(true);
});
});
describe('When Amazon Q code review feedback is not available', () => {
beforeEach(() => {
useNotes().setUserData(userDataMock);
wrapper = mountNoteActions({
...props,
isAmazonQCodeReview: false,
canEdit: true,
canReportAsAbuse: true,
});
});
it('does not render the feedback button when isAmazonQCodeReview is false', () => {
expect(findFeedbackButton().exists()).toBe(false);
});
it('still renders other action buttons correctly', () => {
// Verify that other buttons like delete are still rendered
expect(findDeleteButton().exists()).toBe(true);
});
});
describe('report abuse button', () => {
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);

View File

@ -77,7 +77,8 @@ describe('GlobalSearchStatusBar', () => {
});
it('renders the status bar', () => {
expect(wrapper.text()).toContain('Showing 3000 code results for test');
expect(wrapper.text()).toContain('Showing 3000 code results for test.');
expect(wrapper.text()).toContain('Showing 120 out of 1074 files');
});
});
@ -95,7 +96,8 @@ describe('GlobalSearchStatusBar', () => {
});
it('renders the status bar', () => {
expect(wrapper.text()).toContain('Showing 1 code result for test');
expect(wrapper.text()).toContain('Showing 1 code result for test.');
expect(wrapper.text()).toContain('Showing file 1 from 1');
});
});
});
@ -117,8 +119,9 @@ describe('GlobalSearchStatusBar', () => {
it('renders the status bar', () => {
expect(wrapper.text()).toContain(
'Showing 3000 code results for test in group Group Full Name',
'Showing 3000 code results for test in group Group Full Name.',
);
expect(wrapper.text()).toContain('Showing 120 out of 1074 files');
});
it('renders link with proper url', () => {
@ -148,7 +151,10 @@ describe('GlobalSearchStatusBar', () => {
});
it('renders the status bar', () => {
expect(wrapper.text()).toContain('Showing 1 code result for test in group Group Full Name');
expect(wrapper.text()).toContain(
'Showing 1 code result for test in group Group Full Name.',
);
expect(wrapper.text()).toContain('Showing file 1 from 1');
});
it('renders link with proper url', () => {
@ -175,8 +181,9 @@ describe('GlobalSearchStatusBar', () => {
it('renders the status bar', () => {
expect(wrapper.text()).toContain(
'Showing 3000 code results for test in of Project with Namespace',
'Showing 3000 code results for test in of Project with Namespace.',
);
expect(wrapper.text()).toContain('Showing 120 out of 1074 files');
});
it('renders link with proper url', () => {
@ -210,8 +217,9 @@ describe('GlobalSearchStatusBar', () => {
it('renders the status bar', () => {
expect(wrapper.text()).toContain(
'Showing 1 code result for test in of Project with Namespace',
'Showing 1 code result for test in of Project with Namespace.',
);
expect(wrapper.text()).toContain('Showing file 1 from 1');
});
it('renders link with proper url', () => {
@ -246,7 +254,8 @@ describe('GlobalSearchStatusBar', () => {
});
it('does not render the status bar', () => {
expect(wrapper.text()).toBe('Showing 0 code results for test in group Group Full Name');
expect(wrapper.text()).toContain('Showing 0 code results for test in group Group Full Name.');
expect(wrapper.text()).toContain('Showing 10 out of 0 files');
});
});
});

View File

@ -563,6 +563,7 @@ describe('useAccessTokens store', () => {
describe('setup', () => {
it('sets up the store', () => {
store.token = 'new token';
store.setup({
filters,
id,
@ -580,6 +581,7 @@ describe('useAccessTokens store', () => {
expect(store.page).toBe(page);
expect(store.showCreateForm).toBe(true);
expect(store.sorting).toEqual(sorting);
expect(store.token).toEqual(null);
expect(store.urlCreate).toBe(urlCreate);
expect(store.urlRevoke).toBe(urlRevoke);
expect(store.urlRotate).toBe(urlRotate);

View File

@ -11,6 +11,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:variables) do
Gitlab::Ci::Variables::Collection.new([
{ key: 'SUBDIR', value: 'subdir' },
{ key: 'SUBDIR_INVALID', value: 'subdir/does_not_exists' },
{ key: 'FILE_TXT', value: 'file.txt' },
{ key: 'FULL_PATH_VALID', value: 'subdir/my_file.txt' },
{ key: 'FULL_PATH_INVALID', value: 'subdir/does_not_exist.txt' },
@ -55,6 +56,20 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:project) { create(:project, :small_repo, files: files) }
end
context 'when path is a directory' do
let(:globs) { ['$SUBDIR/'] }
context 'when the directory exists' do
it { is_expected.to be_truthy }
end
context 'when the directory does not exist' do
let(:globs) { ['$SUBDIR_INVALID/'] }
it { is_expected.to be_falsey }
end
end
context 'when a file path is in a variable' do
let(:globs) { ['$FULL_PATH_VALID'] }

View File

@ -9,18 +9,33 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, name: 'public-group', developers: current_user) }
let_it_be(:projects) { create_list(:project, 5, :public, group: group) }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, :aimed_for_deletion, group: group) }
let_it_be(:projects) do
(
create_list(:project, 5, :public, group: group) << aimed_for_deletion_project
).each do |project|
create(:ci_pipeline, project: project)
end
end
let_it_be(:other_project) { create(:project, :public, group: group) }
let_it_be(:archived_project) { create(:project, :archived, group: group) }
let(:filters) { {} }
let(:selection) do
"nodes {
#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])}
pipeline {
detailedStatus {
label
}
}
}"
end
let(:query) do
graphql_query_for(
:projects,
filters,
"nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }"
)
graphql_query_for(:projects, filters, selection)
end
context 'when archived argument is ONLY' do
@ -103,11 +118,7 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
let(:filters) { { full_paths: project_full_paths } }
let(:single_project_query) do
graphql_query_for(
:projects,
{ full_paths: [project_full_paths.first] },
"nodes {#{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} }"
)
graphql_query_for(:projects, { full_paths: [project_full_paths.first] }, selection)
end
it_behaves_like 'a working graphql query that returns data' do
@ -119,17 +130,15 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
it 'avoids N+1 queries', :use_sql_query_cache, :clean_gitlab_redis_cache do
post_graphql(single_project_query, current_user: current_user)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
control = ActiveRecord::QueryRecorder.new(skip_cached: false, query_recorder_debug: true) do
post_graphql(single_project_query, current_user: current_user)
end
# There is an N+1 query related to custom roles - https://gitlab.com/gitlab-org/gitlab/-/issues/515675
# There is an N+1 query for duo_features_enabled cascading setting - https://gitlab.com/gitlab-org/gitlab/-/issues/442164
# There is an N+1 query related to pipelines - https://gitlab.com/gitlab-org/gitlab/-/issues/515677
# There is an N+1 query related to marked_for_deletion - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
expect do
post_graphql(query, current_user: current_user)
end.not_to exceed_all_query_limit(control).with_threshold(12)
end.not_to exceed_all_query_limit(control).with_threshold(9)
end
it 'returns the expected projects' do
@ -201,16 +210,6 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
end
context 'when providing the not_aimed_for_deletion argument' do
let_it_be(:project_aimed_for_deletion1) do
create(:project, :public, marked_for_deletion_at: 1.day.ago, group: group)
end
let_it_be(:project_aimed_for_deletion2) do
create(:project, :public, marked_for_deletion_at: 3.days.ago, group: group)
end
let_it_be(:project_not_aimed_for_deletion) { create(:project, :public, group: group) }
let(:filters) { { not_aimed_for_deletion: true, archived: :INCLUDE } }
before do
@ -220,17 +219,15 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
it 'returns only projects not aimed for deletion' do
expect(graphql_data_at(:projects, :nodes))
.to contain_exactly(
*projects.map { |project| a_graphql_entity_for(project) },
*(projects - [aimed_for_deletion_project]).map { |project| a_graphql_entity_for(project) },
a_graphql_entity_for(other_project),
a_graphql_entity_for(archived_project),
a_graphql_entity_for(project_not_aimed_for_deletion)
a_graphql_entity_for(archived_project)
)
end
it 'excludes projects marked for deletion' do
expect(graphql_data_at(:projects, :nodes)).not_to include(
a_graphql_entity_for(project_aimed_for_deletion1),
a_graphql_entity_for(project_aimed_for_deletion2)
a_graphql_entity_for(aimed_for_deletion_project)
)
end
end
@ -255,8 +252,14 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
returned_ids = returned_projects.pluck('id')
returned_marked_for_deletion_on = returned_projects.pluck('markedForDeletionOn')
expect(returned_ids).to contain_exactly(project_marked_for_deletion.to_global_id.to_s)
expect(returned_marked_for_deletion_on).to contain_exactly(marked_for_deletion_on.iso8601)
expect(returned_ids).to contain_exactly(
project_marked_for_deletion.to_global_id.to_s,
aimed_for_deletion_project.to_global_id.to_s
)
expect(returned_marked_for_deletion_on).to contain_exactly(
project_marked_for_deletion.marked_for_deletion_on.iso8601,
aimed_for_deletion_project.marked_for_deletion_on.iso8601
)
end
end