diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
index bb3327d91f0..c659679d46f 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -258,7 +258,7 @@ export default {
:is(ul, ol) > li {
+ .has-task-list-item-actions > :is(ul, ol) > li {
margin-inline-end: 1.5rem;
}
@@ -36,10 +36,6 @@
inset-inline-start: -0.6rem;
}
}
-
- .dropdown-item.text-danger p {
- color: var(--red-500, $red-500); /* Override typography.scss making text black */
- }
}
.is-ghost {
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 6c1fdec7147..2b43a5ad7d7 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -345,12 +345,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
}
}
-/** Ideally should be fixed in gitlab-ui but fixing it using classes for now **/
-.work-item-sidebar-dropdown-toggle {
- justify-content: start !important;
-}
-
-.work-item-sidebar-dropdown-toggle ~ .gl-new-dropdown-panel {
+.work-item-sidebar-dropdown .gl-new-dropdown-panel {
width: 100% !important;
max-width: 19rem !important;
}
diff --git a/app/assets/stylesheets/pages/colors.scss b/app/assets/stylesheets/pages/colors.scss
index c9952fbcc7a..8cbf3e8cbb8 100644
--- a/app/assets/stylesheets/pages/colors.scss
+++ b/app/assets/stylesheets/pages/colors.scss
@@ -67,7 +67,7 @@
&:nth-of-type(7) {
border-top-right-radius: $gl-border-radius-base;
}
-
+
&:nth-last-child(7) {
border-bottom-left-radius: $gl-border-radius-base;
}
@@ -78,11 +78,3 @@
}
}
}
-
-.suggested-colors {
- .color-palette {
- width: 28px;
- height: 28px;
- border-radius: 2px;
- }
-}
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index b1efa39fba1..36a61487a8e 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -136,6 +136,7 @@ module IssuesHelper
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)),
full_path: namespace.full_path,
+ has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s,
initial_sort: current_user&.user_preference&.issues_sort,
is_issue_repositioning_disabled: issue_repositioning_disabled?.to_s,
is_public_visibility_restricted:
@@ -143,7 +144,7 @@ module IssuesHelper
is_signed_in: current_user.present?.to_s,
rss_path: url_for(safe_params.merge(rss_url_options)),
sign_in_path: new_user_session_path,
- has_issue_date_filter_feature: has_issue_date_filter_feature?(namespace, current_user).to_s
+ wi: work_items_show_data(namespace)
}
end
@@ -179,10 +180,7 @@ module IssuesHelper
quick_actions_help_path: help_page_path('user/project/quick_actions'),
releases_path: project_releases_path(project, format: :json),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),
- show_new_issue_link: show_new_issue_link?(project).to_s,
- report_abuse_path: add_category_abuse_reports_path,
- register_path: new_user_registration_path(redirect_to_referer: 'yes'),
- issues_list_path: project_issues_path(project)
+ show_new_issue_link: show_new_issue_link?(project).to_s
)
end
@@ -191,11 +189,10 @@ module IssuesHelper
can_create_projects: can?(current_user, :create_projects, group).to_s,
can_read_crm_contact: can?(current_user, :read_crm_contact, group).to_s,
can_read_crm_organization: can?(current_user, :read_crm_organization, group).to_s,
+ group_id: group.id,
has_any_issues: @has_issues.to_s,
has_any_projects: @has_projects.to_s,
- new_project_path: new_project_path(namespace_id: group.id),
- group_id: group.id,
- issues_list_path: issues_group_path(group)
+ new_project_path: new_project_path(namespace_id: group.id)
)
end
diff --git a/app/services/resource_access_tokens/revoke_service.rb b/app/services/resource_access_tokens/revoke_service.rb
index 46c71b04632..f39cd0144eb 100644
--- a/app/services/resource_access_tokens/revoke_service.rb
+++ b/app/services/resource_access_tokens/revoke_service.rb
@@ -19,11 +19,15 @@ module ResourceAccessTokens
access_token.revoke!
- destroy_bot_user
+ success_message = "Access token #{access_token.name} has been revoked"
+ unless Feature.enabled?(:retain_resource_access_token_user_after_revoke, resource)
+ destroy_bot_user
+ success_message += " and the bot user has been scheduled for deletion"
+ end
log_event
- success("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.")
+ success("#{success_message}.")
rescue StandardError => error
log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
error(error.message)
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
index 7ba4cd6d733..f054e2a8cbb 100644
--- a/app/views/admin/cohorts/index.html.haml
+++ b/app/views/admin/cohorts/index.html.haml
@@ -1,4 +1,13 @@
-- page_title _("Users")
+- add_to_breadcrumbs _("Users"), admin_users_path
+- breadcrumb_title _("Cohorts")
+- page_title _("Cohorts"), _("Users")
+
+= render ::Layouts::PageHeadingComponent.new(_('Cohorts')) do |c|
+ - c.with_actions do
+ = render_if_exists 'admin/users/admin_email_users'
+ = render_if_exists 'admin/users/admin_export_user_permissions'
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
+ = s_('AdminUsers|New user')
= render 'admin/users/tabs'
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index bb06b5cd036..3f0b820f26d 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -10,7 +10,6 @@
.gl-display-flex.gl-align-items-flex-start.gl-flex-wrap.gl-md-flex-nowrap.gl-gap-4.row-content-block.gl-border-0{ data: { testid: "filtered-search-block" } }
#js-admin-users-filter-app
.gl-flex-shrink-0
- = label_tag s_('AdminUsers|Sort by')
= gl_redirect_listbox_tag admin_users_sort_options(filter: params[:filter], search_query: params[:search_query]), @sort, data: { placement: 'right' }
#js-admin-users-app{ data: admin_users_data_attributes(@users) }
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index b1ad2bda6da..02db356d141 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,13 +1,15 @@
- page_title _("Users")
-.top-area{ data: { event_tracking_load: 'true', event_tracking: 'view_admin_users_pageload' } }
- = render 'tabs'
- .nav-controls
+= render ::Layouts::PageHeadingComponent.new(_('Users'), options: { data: { event_tracking_load: 'true', event_tracking: 'view_admin_users_pageload' } }) do |c|
+ - c.with_actions do
= render_if_exists 'admin/users/admin_email_users'
= render_if_exists 'admin/users/admin_export_user_permissions'
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_path) do
= s_('AdminUsers|New user')
+.top-area
+ = render 'tabs'
+
.tab-content
.tab-pane.active
= render 'users'
diff --git a/app/views/shared/wikis/404.html.haml b/app/views/shared/wikis/404.html.haml
index 2ebfc1be440..f36b64e12a9 100644
--- a/app/views/shared/wikis/404.html.haml
+++ b/app/views/shared/wikis/404.html.haml
@@ -1,5 +1,7 @@
- page_title _("Wiki")
- @right_sidebar = true
+- @gfm_form = true
+- @noteable_type = 'Wiki'
- add_page_specific_style 'page_bundles/wiki'
- if @error.present?
diff --git a/config/feature_flags/beta/retain_resource_access_token_user_after_revoke.yml b/config/feature_flags/beta/retain_resource_access_token_user_after_revoke.yml
new file mode 100644
index 00000000000..04da64070ba
--- /dev/null
+++ b/config/feature_flags/beta/retain_resource_access_token_user_after_revoke.yml
@@ -0,0 +1,9 @@
+---
+name: retain_resource_access_token_user_after_revoke
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/462217
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157130
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/468606
+milestone: '17.2'
+group: group::authentication
+type: beta
+default_enabled: false
diff --git a/doc/.vale/gitlab/LatinTerms.yml b/doc/.vale/gitlab/LatinTerms.yml
index 9fbaf278da9..dd858564e09 100644
--- a/doc/.vale/gitlab/LatinTerms.yml
+++ b/doc/.vale/gitlab/LatinTerms.yml
@@ -2,6 +2,7 @@
# Warning: gitlab.LatinTerms
#
# Checks for use of Latin terms.
+# Uses https://github.com/errata-ai/Google/blob/master/Google/Latin.yml for ideas.
#
# For a list of all options, see https://vale.sh/docs/topics/styles/
extends: substitution
@@ -11,8 +12,6 @@ level: warning
nonword: true
ignorecase: true
swap:
- e\.g\.: for example
- e\. g\.: for example
- i\.e\.: that is
- i\. e\.: that is
- via: "with', 'through', or 'by using"
+ '\b(?:e\.?g[\s.,;:])': for example
+ '\b(?:i\.?e[\s.,;:])': that is
+ '\bvia\b': "with', 'through', or 'by using"
diff --git a/doc/administration/settings/jira_cloud_app.md b/doc/administration/settings/jira_cloud_app.md
index 7d2ec1ec19c..3f39d305e34 100644
--- a/doc/administration/settings/jira_cloud_app.md
+++ b/doc/administration/settings/jira_cloud_app.md
@@ -97,10 +97,12 @@ Alternatively, you might want to [install the GitLab for Jira Cloud app manually
- The instance must be on GitLab version 15.7 or later.
- You must set up [OAuth authentication](#set-up-oauth-authentication).
- If your instance uses HTTPS, your GitLab certificate must be publicly trusted or contain the full chain certificate.
-- Your network must allow inbound and outbound connections between GitLab and Jira. For self-managed instances that are behind a
+- Your network must allow inbound and outbound connections between your self-managed instance,
+ Jira, and GitLab.com. For self-managed instances that are behind a
firewall and cannot be directly accessed from the internet, you must:
1. Set up an internet-facing [reverse proxy](#using-a-reverse-proxy) in front of your self-managed instance.
1. Open your firewall and allow inbound traffic from [Atlassian IP addresses](https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/#Outgoing-Connections) only.
+ 1. Add [GitLab IP addresses](../../user/gitlab_com/index.md#ip-range) to the allowlist of your firewall.
- The Jira user that installs and configures the app must meet certain [requirements](#jira-user-requirements).
### Set up your instance
diff --git a/doc/administration/settings/jira_cloud_app_troubleshooting.md b/doc/administration/settings/jira_cloud_app_troubleshooting.md
index c9bc222ba5d..3748b83f4e6 100644
--- a/doc/administration/settings/jira_cloud_app_troubleshooting.md
+++ b/doc/administration/settings/jira_cloud_app_troubleshooting.md
@@ -221,7 +221,9 @@ For the second log, you might have one of the following scenarios:
- `json.jira_status_code` and `json.jira_body` might contain the response received from the self-managed instance or a proxy in front of the instance.
- If `json.jira_status_code` is `401 Unauthorized` and `json.jira_body` is `(empty)`:
- [**Jira Connect Proxy URL**](jira_cloud_app.md#set-up-your-instance) might not be set to `https://gitlab.com`.
- - The self-managed instance might be blocking outgoing connections. Ensure that the self-managed instance can connect to `connect-install-keys.atlassian.com`.
+ - The self-managed instance might be blocking outgoing connections. Ensure that your
+ self-managed instance can connect to both `connect-install-keys.atlassian.com`
+ and `gitlab.com`.
- The self-managed instance is unable to decrypt the JWT token from Jira. [From GitLab 16.11](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147234),
the [`exceptions_json.log`](../logs/index.md#exceptions_jsonlog) contains more information about the error.
- If a [reverse proxy](jira_cloud_app.md#using-a-reverse-proxy) is in front of your self-managed instance,
diff --git a/doc/architecture/blueprints/ai_context_management/img/architecture.jpg b/doc/architecture/blueprints/ai_context_management/img/architecture.jpg
new file mode 100644
index 00000000000..a0ec711c439
Binary files /dev/null and b/doc/architecture/blueprints/ai_context_management/img/architecture.jpg differ
diff --git a/doc/architecture/blueprints/ai_context_management/index.md b/doc/architecture/blueprints/ai_context_management/index.md
new file mode 100644
index 00000000000..28e5a53057c
--- /dev/null
+++ b/doc/architecture/blueprints/ai_context_management/index.md
@@ -0,0 +1,290 @@
+---
+status: proposed
+creation-date: "2023-06-03"
+authors: [ "@dmishunov" ]
+coach: "@jessieay"
+approvers: [ ]
+owning-stage: "~devops::ai-powered"
+participating-stages: ["~devops::create"]
+---
+
+# AI Context Management
+
+## Glossary
+
+- **AI Context**. In the scope of this technical blueprint, the term "AI Context" refers to supplementary information
+provided to the AI system alongside the primary prompts.
+- **AI Context Policy**. The "AI Context Policy" is a user-defined and user-managed mechanism allowing precise
+control over the content that can be sent to the AI as contextual information. In the context of this blueprint, the
+_AI Context Policy_ is suggested as a YAML configuration file.
+- **AI Context Policy Management**. Within this blueprint, "Management" encompasses the user-driven processes of
+creating, modifying, and removing AI Context Policies according to specific requirements and preferences.
+- **Automatic AI Context**. _AI Context_, retrieved automatically based on the active document. _*Automatic AI Contex_
+can be the active document's dependencies (modules, methods, etc., imported into the active document), some
+search-based, or other mechanisms over which the user has limited control.
+- **Supplementary User Context**: User-defined _AI Context_, such as open tabs in IDEs, local files, and folders, that the user
+provides from their local environment to extend the default _AI Context_.
+- **AI Context Retriever**: A backend system capable of:
+ - communicating with _AI Context Policy Management_
+ - fetching content defined in _Automatic AI Context_ and _Supplementary User Context_ (complete files, definitions,
+ methods, etc.), based on the _AI Context Policy Management_
+ - correctly augment the user prompt with AI Context before sending it to LLM. Presumably, this part is already
+ handled by [AI Gateway](../ai_gateway/index.md).
+- **Project Administrator**. In the context of this blueprint, "Project Administrator" means any individual with the
+"Edit project settings" permission ("Maintainer" or "Owner" roles, as defined in [Project members permissions](../../../user/permissions.md#project-members-permissions)).
+
+
+
+## Summary
+
+Correct context can dramatically improve the quality of AI responses. This blueprint aims to accommodate AI Context
+seamlessly into our offering by architecting a solution that is ready for this additional context coming from different
+AI features.
+
+However, we recognize the importance of security and trust, which automatic solutions do not necessarily provide. To
+address any concerns users might have about the content fed into the AI Context, this blueprint suggests providing them
+with control and customization options. This way, users can adjust the content according to their preferences and have a
+clear understanding of what information is being utilized.
+
+This blueprint proposes a system for managing _AI Context_ at the _Project Administrator_ and individual
+user levels. Its goal is to allow _Project Administrator_ to set high-level rules for what content can be included as context for AI
+prompts while enabling users to specify _Supplementary User Context_ for their prompts. The global _AI Context Policy_ will use a YAML
+configuration file format stored in the same Git repository. The suggested format of the YAML configuration files
+is discussed below.
+
+## Motivation
+
+Ensuring the AI has the correct context is crucial for generating accurate and relevant code suggestions or responses.
+As the adoption of AI-assisted development grows, it's essential to give organizations and users control over what project
+content is sent as context to AI models. Some files or directories may contain sensitive information that should not
+be shared. At the same time, users may want to provide additional context for their prompts to get more
+relevant suggestions. We need a flexible _AI Context_ management system to handle these cases.
+
+### Goals
+
+### For _Project Administrators_
+
+- Allow _Project Administrators_ set the default _AI Context Policy_ to control whether content can or cannot be
+automatically included in the _AI Context_ when making requests to LLMs
+- Allow _Project Administrators_ to specify exceptions to the default _AI Context Policy_
+- Provide a UI to manage the default _AI Context Policy_ and its exceptions list easily
+
+### For users
+
+- Allow to set _Supplementary User Context_ to include as AI context for their prompts
+- Provide a UI to manage _Supplementary User Context_ easily
+
+### Non-Goals
+
+- _AI Context Retriever_ architecture - different environments (Web, IDEs) will probably implement their retrievers.
+However, the unified public interface of the retrievers should be considered.
+- Extremely granular controls like allowing/excluding individual lines of code
+- Storing entire file contents from user projects, only paths will be persisted
+
+## Proposal
+
+The proposed architecture consists of 3 main parts:
+
+- _AI Context Retriever_
+- _AI Context Policy Management_
+- _Supplementary User Context_
+
+There are several different ongoing efforts related to various implementations of _AI Context Retriever_ both
+[for Web](https://gitlab.com/groups/gitlab-org/-/epics/14040), and [for IDEs](https://gitlab.com/groups/gitlab-org/editor-extensions/-/epics/55).
+Because of that, the architecture for _AI Context Retriever_ is beyond the scope of this blueprint. However, in the
+context of this blueprint, it is assumed that:
+
+- _AI Context Retriever_ is capable of automatically retrieving and fetching _Automatic AI Context_ and passing it
+on as _AI Context_ to LLM.
+- _AI Context Retriever_ can automatically retrieve and fetch _Supplementary User Context_and pass
+it on as _AI Context_ to LLM.
+- _AI Context Retriever_ implementation can ensure that any content passed as _AI Context_ to a model
+adheres to the global _AI Context Policy_.
+- _AI Context Retriever_ can trim the _AI Context_ to meet the contextual window requirement for a
+specific LLM used for that or another Duo feature.
+
+### _AI Context Policy Management_ proposal
+
+To implement the _AI Context Policy Management_ system, it is proposed to:
+
+- Introduce the YAML file format for configuring global policies
+- In the YAML configuration file, support two `ai_context_policy` types:
+ - `block`: blocks all content except for the specified `exclude` paths. Excluded files are allowed. (**Default**)
+ - `allow`: allows all content except for the specified `exclude` paths. Excluded files are blocked.
+ - `version`: specifies the schema version of the AI context file. Starting with `version: 1`. If omitted treated as the latest version known to the client.
+- In the YAML configuration file, support glob patterns to exclude certain paths from the global policy
+- Support nested _AI Context Policies_ to provide a more granular control of _AI Context_ in sub-folders. For
+example, a policy in `/src/tests` would override a policy in `/src`, which, in its turn, would override a
+global _AI Context Policy_ in `/`.
+
+### _Supplementary User Context_ proposal
+
+To implement the _Supplementary User Context_ system, it is proposed to:
+
+- Introduce user-level UI to specify _Supplementary User Context_ for prompts. A particular implementation of the UI could
+differ in different environments (IDEs, Web, etc.), but the actual design of these implementations is beyond the scope of
+this architecture blueprint
+- The user-level UI should communicate to the user what is in the _Supplementary User Context_ at any moment.
+- The user-level UI should allow the user to edit the contents of the _Supplementary User Context_.
+
+### Optional steps
+
+- Provide UI for _Project Administrators_ to configure global _AI Context Policy_. [Source Editor](../../../development/fe_guide/source_editor.md)
+can be used as the editor for this type of YAML file format, similar to the
+[Security Policy Editor](../../../user/application_security/policies/index.md#policy-editor).
+- Implement a validation mechanism for _AI Context Policies_ to somehow notify the _Project Administrators_ in case
+of the invalid format of the YAML configuration file. It could be a job in CI. But to catch possible issues proactively, it is
+also advised to introduce the validation step as part of the
+[pre-push static analysis](../../../development/contributing/style_guides.md#pre-push-static-analysis-with-lefthook)
+
+## Design and implementation details
+
+- **YAML Configuration File Format**: The proposed YAML configuration file format for defining the global
+_AI Context Policy_ is as follows:
+
+ ```yaml
+ ai_context_policy: [allow|block]
+
+ exclude:
+ - glob/**/pattern
+ ```
+
+ The `ai_context_policy` section specifies the current policy for this and all underlying folders in a repo.
+
+ The `exclude` section specifies the exceptions to the `ai_context_policy`. Technically, it's an inversion of the policy.
+ For example, if we specify `foo_bar.js` in `exclude`:
+
+ - for the `allow` policy, it means that `foo_bar.js` will be blocked
+ - for the `block` policy, it means that `foo_bar.js` will be allowed
+
+- **User-Level UI for _Supplementary User Context_**: The UI for specifying _Supplementary User Context_ for prompts
+can be implemented differently depending on the environment (IDEs, Web, etc.). However, the implementation should
+ensure users can provide additional context for their prompts. The specified _Supplementary User Context_ for
+each user can be stored as:
+
+ - a preference stored in the user profile in GitLab
+
+ - **Pros**: Consistent across devices and environments (Web, IDEs, etc.)
+ - **Cons**: Additional work in the monolith, potentially a lot of new read/writes to a database
+
+ - a preference stored in the local IDE/Web storage
+
+ - **Pros**: User-centric, local to user environment
+ - **Cons**: Different implementations for different environments (Web, IDEs, etc.), doesn't survive switching
+ environment or device
+
+ In both cases, the storage should allow the preference to be associated with a particular repository. Factors
+ like data consistency, performance, and implementation complexity should guide the decision on what type of storage
+ to use.
+
+- To mitigate potential performance and scalability issues, it would make sense to keep _AI Context Retriever_, and
+_AI Context Policy Management_ in the same environment as the feature needing those. It would be
+[Language Server](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp) for Duo features in IDEs and different
+services in the monolith for Duo features on the Web.
+
+### Data flow
+
+Here's the draft of the data flow demonstrating the role of _AI Context_ using the Code Suggestions feature as an example.
+
+```mermaid
+sequenceDiagram
+ participant CS as Code Suggestions
+ participant CR as AI Context Retriever
+ participant PM as AI Context Policy Management
+ participant LLM as Language Model
+
+ CS->>CR: Request Code Suggestion
+ CR->>CR: Retrieve Supplementary User Context list
+ CR->>CR: Retrieve Automatic AI Context list
+ CR->>PM: Check AI Context against Policy
+ PM-->>CR: Return valid AI Context list
+ CR->>CR: Fetch valid AI Context
+ CR->>LLM: Send prompt with final AI Context
+ LLM->>LLM: Generate code suggestions
+ LLM-->>CS: Return code suggestions
+ CS->>CS: Present code suggestions to the user
+```
+
+In case the _AI Context Retriever_ fails to fetch any content from the _AI Context_, the prompt is sent with
+_AI Context_, which was successfully fetched. In a low-probability case, when _AI Context Retriever_ cannot fetch any content, the prompt should be sent out as-is.
+
+## Alternative solutions
+
+### JSON Configuration Files
+
+- **Pros**: Widely used, easier integration with web technologies.
+- **Cons**: Less readable compared to YAML for complex configurations.
+
+### Database-Backed Configuration
+
+- **Pros**: Centralized management, dynamic updates.
+- **Cons**: Not version controlled.
+
+### Environment Variables
+
+- **Pros**: Simplifies configuration for deployment and scaling.
+- **Cons**: Less suitable for complex configurations.
+
+### Policy as Code (without YAML)
+
+- **Pros**: Better control and auditing with versioned code.
+- **Cons**: It requires users to write code and us to invent a language for it.
+
+### Policy in `.ai_ignore` and other Git-like files
+
+- **Pros**: Provides a straightforward approach, identical to the `allow` policy with the list of `exclude` suggested in this blueprint
+- **Cons**: Supports only the `allow` policy; the processing of this file type still has to be implemented
+
+Based on these alternatives, the YAML file was chosen as a format for this blueprint because of versioning
+in Git, and more versatility compared to the `.ai_ignore` alternative.
+
+## Suggested iterative implementation plan
+
+Please refer to the [Proposal](#proposal) for a detailed explanation of the items in every iteration.
+
+### Iteration 1
+
+- Introduce the global `.ai-context-policy.yaml` YAML configuration file format and schema for this file type
+as part of _AI Context Policy Management_.
+- _AI Context Retrievers_ introduce support for _Supplementary User Context_.
+- Optional: validation mechanism (like CI job and pre-push static analysis) for `.ai-context-policy.yaml`
+
+**Success criteria for the iteration:** Prompts sent from the Code Suggestions feature in IDEs contain
+_AI Context_ only with the open IDE tabs, which adhere to the global _AI Context Policy_ in the root of a repository.
+
+### Iteration 2
+
+- In _AI Context Retrievers_ introduce support for _Automatic AI Context_.
+- Connect more features to the _AI Context Management_ system.
+
+**Success criteria for the iteration:** Prompts sent from the Code Suggestions feature in IDEs contain _AI Context_
+with items of _Automatic AI Context_, which adhere to the global _AI Context Policy_ in the root of a repository.
+
+### Iteration 3
+
+- Connect all Duo features on the Web and in IDEs to _AI Context Retrievers_ and adhere to the global
+_AI Context Policy_.
+
+**Success criteria for the iteration:** All Duo features in all environments send _AI Context_ which adheres to the
+global _AI Context Policy_
+
+### Iteration 4
+
+- Support nested `.ai-context-policy.yaml` YAML configuration files.
+
+**Success criteria for the iteration:** _AI Context Policy_ placed into the sub-folders of a repository, override
+higher-level policies when sending prompts.
+
+### Iteration 5
+
+- User-level UI for _Supplementary User Context_.
+
+**Success criteria for the iteration:** Users can see and edit the contents of the _Supplementary User Context_ and
+the context is shared between all Duo features within the environment (Web, IDEs, etc.)
+
+### Iteration 6
+
+- Optional: UI for configuring the global _AI Context Policy_.
+
+**Success criteria for the iteration:** Users can see and edit the contents of the _AI Context Policies_ in a UI
+editor.
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index 38c1cf70038..e550d81936f 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -768,14 +768,13 @@ We want to avoid introducing a changelog when features are not accessible by an
ACF(added / changed / fixed / '...')
RF{Remove flag}
RF2{Remove flag}
- NC(No changelog)
RC(removed / changed)
OTHER(other)
FDOFF -->CDO-->ACF
FDOFF -->RF
RF-->|Keep new code?| ACF
- RF-->|Keep old code?| NC
+ RF-->|Keep old code?| OTHER
FDON -->RF2
RF2-->|Keep old code?| RC
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index d95010ae076..8108ddf74b7 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -109,6 +109,11 @@ Even when creation is disabled, you can still use and revoke existing project ac
## Bot users for projects
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/462217) in GitLab 17.2 [with a flag](../../../administration/feature_flags.md) named `retain_resource_access_token_user_after_revoke`. Disabled by default. When enabled, the bot user is retained. It is not deleted and its records are not moved to the Ghost User.
+
+FLAG:
+The behavior of the bot user after the project access token is revoked is controlled by a feature flag. For more information, see the history.
+
Bot users for projects are [GitLab-created service accounts](../../../subscriptions/self_managed/index.md#billable-users).
Each time you create a project access token, a bot user is created and added to the project.
This user is not a billable user, so it does not count toward the license limit.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3c7f47b2121..3c936ac7a8d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4467,9 +4467,6 @@ msgstr ""
msgid "AdminUsers|Skype"
msgstr ""
-msgid "AdminUsers|Sort by"
-msgstr ""
-
msgid "AdminUsers|Stop monitoring %{username} for possible spam?"
msgstr ""
@@ -5514,6 +5511,9 @@ msgstr ""
msgid "An error occurred while checking group path. Please refresh and try again."
msgstr ""
+msgid "An error occurred while creating the group. Please try again."
+msgstr ""
+
msgid "An error occurred while creating the issue. Please try again."
msgstr ""
@@ -12916,6 +12916,9 @@ msgstr ""
msgid "CodeownersValidation|Zero owners"
msgstr ""
+msgid "Cohorts"
+msgstr ""
+
msgid "Cohorts|Active users"
msgstr ""
@@ -20305,6 +20308,9 @@ msgstr ""
msgid "Enter epic URL"
msgstr ""
+msgid "Enter group name"
+msgstr ""
+
msgid "Enter in your Bitbucket Server URL and personal access token below"
msgstr ""
@@ -51740,10 +51746,13 @@ msgstr ""
msgid "SubscriptionBanner|Upload new license"
msgstr ""
+msgid "SubscriptionGroupsNew|Group name %{error}"
+msgstr ""
+
msgid "SubscriptionGroupsNew|Select a group for your %{planName} subscription"
msgstr ""
-msgid "SubscriptionGroupsNew|Select a group for your subscription"
+msgid "SubscriptionGroupsNew|Select a group for your subscription."
msgstr ""
msgid "SubscriptionGroupsNew|The group is a top-level group on a Free tier"
@@ -51755,6 +51764,9 @@ msgstr ""
msgid "SubscriptionGroupsNew|You're assigned the Owner role of the group"
msgstr ""
+msgid "SubscriptionGroupsNew|Your group will be created at:"
+msgstr ""
+
msgid "SubscriptionGroupsNew|Your group will only be displayed in the list above if:"
msgstr ""
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index 82f5c6b3eee..a79609b72f2 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -20,7 +20,7 @@ describe('TaskListItemActions component', () => {
document.body.appendChild(li);
wrapper = shallowMountExtended(TaskListItemActions, {
- provide: { canUpdate: true, issuableType },
+ provide: { issuableType },
attachTo: document.querySelector('div'),
});
};
@@ -32,8 +32,8 @@ describe('TaskListItemActions component', () => {
category: 'tertiary',
icon: 'ellipsis_v',
placement: 'bottom-end',
- toggleText: TaskListItemActions.i18n.taskActions,
textSrOnly: true,
+ toggleText: 'Task actions',
});
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 2a4d90ce7cc..a8982ac76cd 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -109,6 +109,8 @@ describe('WorkItemLabels component', () => {
const findRegularLabel = () => findAllLabels().at(0);
const findLabelWithDescription = () => findAllLabels().at(2);
const findDropdownContentsCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
+ const findCreateLabelButton = () => wrapper.findByTestId('create-label');
+ const findManageLabelsButton = () => wrapper.findByTestId('manage-labels');
const showDropdown = () => {
findWorkItemSidebarDropdownWidget().vm.$emit('dropdownShown');
@@ -422,71 +424,94 @@ describe('WorkItemLabels component', () => {
});
});
- describe('creating project label', () => {
- beforeEach(async () => {
- createComponent();
+ describe('create/manage label buttons', () => {
+ describe('when project context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: false });
+ });
- wrapper.findByTestId('create-project-label').vm.$emit('click');
- await nextTick();
- });
+ it('renders "Create project label" button', () => {
+ expect(findCreateLabelButton().text()).toBe('Create project label');
+ });
- describe('when "Create project label" button is clicked', () => {
- it('renders "Create label" dropdown', () => {
- expect(findDisclosureDropdown().props()).toMatchObject({
- block: true,
- startOpened: true,
- toggleText: 'No labels',
- });
- expect(findDropdownContentsCreateView().props()).toEqual({
- attrWorkspacePath: 'test-project-path',
- fullPath: 'test-project-path',
- labelCreateType: 'project',
- searchKey: '',
- workspaceType: 'project',
- });
+ it('renders "Manage project labels" link', () => {
+ expect(findManageLabelsButton().text()).toBe('Manage project labels');
+ expect(findManageLabelsButton().attributes('href')).toBe('test-project-path/labels');
});
});
- describe('when "hideCreateView" event is emitted', () => {
- it('hides dropdown', async () => {
- expect(findDisclosureDropdown().exists()).toBe(true);
- expect(findDropdownContentsCreateView().exists()).toBe(true);
+ describe('when group context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ });
- findDropdownContentsCreateView().vm.$emit('hideCreateView');
+ it('renders "Create group label" button', () => {
+ expect(findCreateLabelButton().text()).toBe('Create group label');
+ });
+
+ it('renders "Manage group labels" link', () => {
+ expect(findManageLabelsButton().text()).toBe('Manage group labels');
+ expect(findManageLabelsButton().attributes('href')).toBe('test-project-path/labels');
+ });
+ });
+
+ describe('creating project label', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findCreateLabelButton().vm.$emit('click');
await nextTick();
-
- expect(findDisclosureDropdown().exists()).toBe(false);
- expect(findDropdownContentsCreateView().exists()).toBe(false);
});
- });
- describe('when "labelCreated" event is emitted', () => {
- it('updates "createdLabelId" value and hides dropdown', async () => {
- expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(undefined);
- expect(findDisclosureDropdown().exists()).toBe(true);
- expect(findDropdownContentsCreateView().exists()).toBe(true);
-
- findDropdownContentsCreateView().vm.$emit('labelCreated', {
- id: 'gid://gitlab/Label/55',
- name: 'New label',
+ describe('when "Create project label" button is clicked', () => {
+ it('renders "Create label" dropdown', () => {
+ expect(findDisclosureDropdown().props()).toMatchObject({
+ block: true,
+ startOpened: true,
+ toggleText: 'No labels',
+ });
+ expect(findDropdownContentsCreateView().props()).toEqual({
+ attrWorkspacePath: 'test-project-path',
+ fullPath: 'test-project-path',
+ labelCreateType: 'project',
+ searchKey: '',
+ workspaceType: 'project',
+ });
});
- await nextTick();
+ });
- expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(
- 'gid://gitlab/Label/55',
- );
- expect(findDisclosureDropdown().exists()).toBe(false);
- expect(findDropdownContentsCreateView().exists()).toBe(false);
+ describe('when "hideCreateView" event is emitted', () => {
+ it('hides dropdown', async () => {
+ expect(findDisclosureDropdown().exists()).toBe(true);
+ expect(findDropdownContentsCreateView().exists()).toBe(true);
+
+ findDropdownContentsCreateView().vm.$emit('hideCreateView');
+ await nextTick();
+
+ expect(findDisclosureDropdown().exists()).toBe(false);
+ expect(findDropdownContentsCreateView().exists()).toBe(false);
+ });
+ });
+
+ describe('when "labelCreated" event is emitted', () => {
+ it('updates "createdLabelId" value and hides dropdown', async () => {
+ expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(undefined);
+ expect(findDisclosureDropdown().exists()).toBe(true);
+ expect(findDropdownContentsCreateView().exists()).toBe(true);
+
+ findDropdownContentsCreateView().vm.$emit('labelCreated', {
+ id: 'gid://gitlab/Label/55',
+ name: 'New label',
+ });
+ await nextTick();
+
+ expect(findWorkItemSidebarDropdownWidget().props('createdLabelId')).toBe(
+ 'gid://gitlab/Label/55',
+ );
+ expect(findDisclosureDropdown().exists()).toBe(false);
+ expect(findDropdownContentsCreateView().exists()).toBe(false);
+ });
});
});
});
-
- it('renders "Manage project labels" link in dropdown', () => {
- createComponent();
-
- expect(wrapper.findByTestId('manage-project-labels').text()).toBe('Manage project labels');
- expect(wrapper.findByTestId('manage-project-labels').attributes('href')).toBe(
- 'test-project-path/labels',
- );
- });
});
diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb
index 3d44dbf454f..b9654bff174 100644
--- a/spec/requests/api/draft_notes_spec.rb
+++ b/spec/requests/api/draft_notes_spec.rb
@@ -26,7 +26,8 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
expect(response).to have_gitlab_http_status(:ok)
end
- it "returns only draft notes authored by the current user" do
+ it "returns only draft notes authored by the current user",
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/448707' do
get api(base_url, user)
draft_note_ids = json_response.pluck("id")
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index b46299301b9..6c575aeb301 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -243,13 +243,49 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
end
context "when the user has valid permissions" do
+ context "when retain bot user ff is disabled" do
+ before do
+ stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
+ end
+
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(token.reload).to be_revoked
+ expect(
+ Users::GhostUserMigration.where(user: project_bot, initiator_user: user)
+ ).to be_exists
+ end
+
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
+ let_it_be(:other_project_bot) { create(:user, :project_bot) }
+ let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
+ let_it_be(:token_id) { other_token.id }
+
+ before do
+ resource.add_maintainer(other_project_bot)
+ end
+
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(token.reload).not_to be_revoked
+ expect(other_token.reload).to be_revoked
+ expect(
+ Users::GhostUserMigration.where(user: other_project_bot, initiator_user: user)
+ ).to be_exists
+ end
+ end
+ end
+
it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
- expect(
- Users::GhostUserMigration.where(user: project_bot, initiator_user: user)
- ).to be_exists
+ expect(token.reload).to be_revoked
+ expect(User.exists?(project_bot.id)).to be_truthy
end
context "when using #{source_type} access token to DELETE other #{source_type} access token" do
@@ -265,9 +301,9 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
- expect(
- Users::GhostUserMigration.where(user: other_project_bot, initiator_user: user)
- ).to be_exists
+ expect(token.reload).not_to be_revoked
+ expect(other_token.reload).to be_revoked
+ expect(User.exists?(other_project_bot.id)).to be_truthy
end
end
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index aab22cb2815..cec8fb3b902 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -15,26 +15,18 @@ RSpec.describe ResourceAccessTokens::RevokeService, feature_category: :system_ac
shared_examples 'revokes access token' do
it { expect(subject.success?).to be true }
- it { expect(subject.message).to eq("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.") }
+ it { expect(subject.message).to eq("Access token #{access_token.name} has been revoked.") }
- it 'calls delete user worker' do
- expect(DeleteUserWorker).to receive(:perform_async).with(user.id, resource_bot.id, skip_authorization: true)
+ it 'does not call the delete user worker' do
+ expect(DeleteUserWorker).not_to receive(:perform_async)
subject
end
- it 'removes membership of bot user' do
+ it 'bot user retains membership' do
subject
- expect(resource.reload).not_to have_user(resource_bot)
- end
-
- it 'initiates user removal' do
- subject
-
- expect(
- Users::GhostUserMigration.where(user: resource_bot, initiator_user: user)
- ).to be_exists
+ expect(resource.reload).to have_user(resource_bot)
end
it 'logs the event' do
@@ -44,6 +36,34 @@ RSpec.describe ResourceAccessTokens::RevokeService, feature_category: :system_ac
expect(Gitlab::AppLogger).to have_received(:info).with("PROJECT ACCESS TOKEN REVOCATION: revoked_by: #{user.username}, project_id: #{resource.id}, token_user: #{resource_bot.name}, token_id: #{access_token.id}")
end
+
+ context 'with retain user feature flag disabled' do
+ before do
+ stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
+ end
+
+ it { expect(subject.message).to eq("Access token #{access_token.name} has been revoked and the bot user has been scheduled for deletion.") }
+
+ it 'calls delete user worker' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, resource_bot.id, skip_authorization: true)
+
+ subject
+ end
+
+ it 'removes membership of bot user' do
+ subject
+
+ expect(resource.reload).not_to have_user(resource_bot)
+ end
+
+ it 'initiates user removal' do
+ subject
+
+ expect(
+ Users::GhostUserMigration.where(user: resource_bot, initiator_user: user)
+ ).to be_exists
+ end
+ end
end
shared_examples 'rollback revoke steps' do
diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
index ec46c4a9ed8..3c11b2fea64 100644
--- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
@@ -159,27 +159,64 @@ RSpec.shared_examples 'POST resource access tokens available' do
end
RSpec.shared_examples 'PUT resource access tokens available' do
- it 'calls delete user worker' do
- expect(DeleteUserWorker).to receive(:perform_async).with(user.id, access_token_user.id, skip_authorization: true)
+ context "when retain bot user ff is disabled" do
+ before do
+ stub_feature_flags(retain_resource_access_token_user_after_revoke: false)
+ end
+ it 'revokes the token' do
+ subject
+ expect(resource_access_token.reload).to be_revoked
+ end
+
+ it 'calls delete user worker' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, access_token_user.id, skip_authorization: true)
+
+ subject
+ end
+
+ it 'removes membership of bot user' do
+ subject
+
+ resource_bots = if resource.is_a?(Project)
+ resource.bots
+ elsif resource.is_a?(Group)
+ User.bots.id_in(resource.all_group_members.non_invite.pluck(:user_id))
+ end
+
+ expect(resource_bots).not_to include(access_token_user)
+ end
+
+ it 'creates GhostUserMigration records to handle migration in a worker' do
+ expect { subject }.to(
+ change { Users::GhostUserMigration.count }.from(0).to(1))
+ end
+ end
+
+ it 'revokes the token' do
+ subject
+ expect(resource_access_token.reload).to be_revoked
+ end
+
+ it 'does not call delete user worker' do
+ expect(DeleteUserWorker).not_to receive(:perform_async)
subject
end
- it 'removes membership of bot user' do
+ it 'does not remove membership of the bot' do
subject
resource_bots = if resource.is_a?(Project)
resource.bots
elsif resource.is_a?(Group)
- User.bots.id_in(resource.all_group_members.non_invite.pluck_primary_key)
+ User.bots.id_in(resource.all_group_members.non_invite.pluck(:user_id))
end
- expect(resource_bots).not_to include(access_token_user)
+ expect(resource_bots).to include(access_token_user)
end
- it 'creates GhostUserMigration records to handle migration in a worker' do
- expect { subject }.to(
- change { Users::GhostUserMigration.count }.from(0).to(1))
+ it 'does not create GhostUserMigration records to handle migration in a worker' do
+ expect { subject }.not_to change { Users::GhostUserMigration.count }
end
context 'when unsuccessful' do