diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb index 5b4f01f27c1..28e012b3248 100644 --- a/app/graphql/mutations/notes/create/note.rb +++ b/app/graphql/mutations/notes/create/note.rb @@ -24,11 +24,10 @@ module Mutations discussion_id = nil if gid = args[:discussion_id] - discussion = GitlabSchema.find_by_gid(gid) + discussion_id = ::GitlabSchema.parse_gid(gid, expected_type: ::Discussion).model_id + discussion = noteable.notes.find_discussion(discussion_id) authorize_discussion!(discussion) - - discussion_id = discussion.id end super.merge({ diff --git a/app/services/work_items/data_sync/handlers/notes/copy_service.rb b/app/services/work_items/data_sync/handlers/notes/copy_service.rb index 2ad19bca7f8..dbd56b2f483 100644 --- a/app/services/work_items/data_sync/handlers/notes/copy_service.rb +++ b/app/services/work_items/data_sync/handlers/notes/copy_service.rb @@ -17,6 +17,7 @@ module WorkItems @target_noteable = target_noteable @source_parent = source_noteable.resource_parent @target_parent = target_noteable.resource_parent + @new_discussion_ids = {} end def execute @@ -40,7 +41,8 @@ module WorkItems private - attr_reader :current_user, :source_noteable, :target_noteable, :source_parent, :target_parent + attr_reader :current_user, :source_noteable, :target_noteable, :source_parent, :target_parent, + :new_discussion_ids def copy_notes_emoji(notes_ids_map) notes_emoji = ::AwardEmoji.by_awardable('Note', notes_ids_map.keys) @@ -79,19 +81,26 @@ module WorkItems ids.zip(allocated_ids).to_h end + # rubocop: disable Metrics/AbcSize -- Despite being long, this method is straightforward. def new_notes(notes_batch, notes_ids_map) notes_batch.map do |note| + new_discussion_ids[note.discussion_id] ||= Note.new( + noteable_id: target_noteable.id, + noteable_type: target_noteable.class.base_class + ).discussion_id + note.attributes.tap do |attrs| attrs['id'] = notes_ids_map[note.id] attrs['noteable_id'] = target_noteable.id # we want this if we want to use this also to copy notes when promoting issue to epic attrs['noteable_type'] = target_noteable.class.base_class + attrs['discussion_id'] = new_discussion_ids[note.discussion_id] # need to use `try` to be able to handle Issue model and legacy Epic model instances attrs['project_id'] = target_noteable.try(:project_id) attrs['namespace_id'] = target_noteable.try(:namespace_id) || target_noteable.try(:group_id) attrs['imported_from'] = 'none' # maintaining current copy notes implementation - # this data is not changed, but it is being serialized and we need it deserialized for bulk inserts + # this data is not changed, but it is being serialized, and we need it deserialized for bulk inserts attrs['position'] = note.attributes_before_type_cast['position'] attrs['original_position'] = note.attributes_before_type_cast['original_position'] attrs['change_position'] = note.attributes_before_type_cast['change_position'] @@ -104,6 +113,7 @@ module WorkItems end end end + # rubocop: enable Metrics/AbcSize def new_notes_emoji(notes_emoji, notes_ids_map) notes_emoji.map do |note_emoji| diff --git a/config/audit_events/types/api_request_access_with_scope.yml b/config/audit_events/types/api_request_access_with_scope.yml index 015a60fd898..89ae33a93ba 100644 --- a/config/audit_events/types/api_request_access_with_scope.yml +++ b/config/audit_events/types/api_request_access_with_scope.yml @@ -1,6 +1,6 @@ --- name: api_request_access_with_scope -description: A susbset of API requests authenticated by a token with an audited scope +description: A subset of API requests authenticated by a token with an audited scope introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/499461 introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548 feature_category: duo_workflow diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 5813242c3a0..39bcb0b4a9c 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -10097,6 +10097,25 @@ Input type: `PromoteToEpicInput` | `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | | `issue` | [`Issue`](#issue) | Issue after mutation. | +### `Mutation.refreshFindingTokenStatus` + +Input type: `RefreshFindingTokenStatusInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `vulnerabilityId` | [`VulnerabilityID!`](#vulnerabilityid) | Global ID of the Vulnerability whose token status should be refreshed. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | +| `findingTokenStatus` | [`VulnerabilityFindingTokenStatus`](#vulnerabilityfindingtokenstatus) | Updated token status record for the given finding. | + ### `Mutation.refreshStandardsAdherenceChecks` Input type: `RefreshStandardsAdherenceChecksInput` diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 3c9fbdd6aef..d08326a35d9 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -294,11 +294,11 @@ Audit event types belong to the following product categories. |:----------|:---------------------|:------------------|:--------------|:------| | [`project_feature_metrics_dashboard_access_level_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106919) | A project's metrics dashboard access level setting is updated | {{< icon name="check-circle" >}} Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369289) | Project | -### GitLab Duo Workflow +### GitLab Duo Agent Platform | Type name | Event triggered when | Saved to database | Introduced in | Scope | |:----------|:---------------------|:------------------|:--------------|:------| -| [`api_request_access_with_scope`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548) | A susbset of API requests authenticated by a token with an audited scope | {{< icon name="check-circle" >}} Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/499461) | User | +| [`api_request_access_with_scope`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172548) | A subset of API requests authenticated by a token with an audited scope | {{< icon name="check-circle" >}} Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/499461) | User | ### Dynamic application security testing diff --git a/doc/user/group/_index.md b/doc/user/group/_index.md index 00c37365043..40b3f07a148 100644 --- a/doc/user/group/_index.md +++ b/doc/user/group/_index.md @@ -99,12 +99,20 @@ the immediate parent group. ## View groups -To explore all public groups you are a member of: +To explore all public or internal groups: 1. On the left sidebar, select **Search or go to**. 1. Select **View all my groups**. 1. In the upper right, select **Explore groups**. +## View groups you are a member of + +{{< history >}} + +- **Member** tab [introduced](https://gitlab.com/groups/gitlab-org/-/epics/13781) in GitLab 18.2 [with a flag](../../administration/feature_flags/_index.md) named `your_work_groups_vue`. Disabled by default. + +{{< /history >}} + To view groups where you have direct or indirect membership: 1. On the left sidebar, select **Search or go to**. @@ -115,6 +123,8 @@ This page shows groups that you are a member of through: - Membership of a subgroup's parent group. - Direct or inherited membership of a project in the group or subgroup. +If the `your_work_groups_vue` feature flag is enabled, groups that you are a member of appear in the **Member** tab. + ## View a group {{< history >}} @@ -270,6 +280,12 @@ the deletion job will instead restore and unarchive the group, so the group will ### View groups pending deletion +{{< history >}} + +- **Inactive** tab [introduced](https://gitlab.com/groups/gitlab-org/-/epics/13781) in GitLab 18.2 [with a flag](../../administration/feature_flags/_index.md) named `your_work_groups_vue`. Disabled by default. + +{{< /history >}} + To view a list of the subgroups that are pending deletion in a group: 1. On the left sidebar, select **Search or go to** and find your group. @@ -277,6 +293,8 @@ To view a list of the subgroups that are pending deletion in a group: Groups that are marked for deletion are labeled **Pending deletion**. +If the `your_work_groups_vue` feature flag is enabled, groups marked for deletion appear in the **Inactive** tab. + ## Delete a group immediately {{< history >}} diff --git a/doc/user/packages/container_registry/build_and_push_images.md b/doc/user/packages/container_registry/build_and_push_images.md index 260db97bd8e..bc5f8524587 100644 --- a/doc/user/packages/container_registry/build_and_push_images.md +++ b/doc/user/packages/container_registry/build_and_push_images.md @@ -33,7 +33,13 @@ You can use Docker commands to build and push container images to your container docker push registry.example.com/group/project/image ``` -## Configure your `.gitlab-ci.yml` file +## Use GitLab CI/CD + +You can use [GitLab CI/CD](../../../ci/_index.md) to build and push container images to the +Container Registry. You can use CI/CD to test, build, and deploy your project from the container +image you created. + +### Configure your `.gitlab-ci.yml` file You can configure your `.gitlab-ci.yml` file to build and push container images to the container registry. @@ -50,19 +56,26 @@ You can configure your `.gitlab-ci.yml` file to build and push container images - Don't build directly to the `latest` tag because multiple jobs may be happening simultaneously. -## Use GitLab CI/CD +### Use a Docker-in-Docker container image -You can use [GitLab CI/CD](../../../ci/_index.md) to build and push container images to the -Container Registry. You can use CI/CD to test, build, and deploy your project from the container -image you created. +You can use your own Docker-in-Docker (DinD) +container images with the container registry or Dependency Proxy. -### Use a Docker-in-Docker container image from your container registry +Use DinD to build, test, and deploy containerized +applications from your CI/CD pipeline. -You can use your own container images for Docker-in-Docker. +Prerequisites: -1. Set up [Docker-in-Docker](../../../ci/docker/using_docker_build.md#use-docker-in-docker). -1. Update the `image` and `service` to point to your registry. -1. Add a service [alias](../../../ci/services/_index.md#available-settings-for-services). +- Set up [Docker-in-Docker](../../../ci/docker/using_docker_build.md#use-docker-in-docker). + +{{< tabs >}} + +{{< tab title="From the container registry" >}} + +In your `.gitlab-ci.yml` file: + +- Update `image` and `services` to point to your registry. +- Add a service [alias](../../../ci/services/_index.md#available-settings-for-services). Your `.gitlab-ci.yml` should look similar to this: @@ -78,20 +91,14 @@ build: - docker run my-docker-image /script/to/run/tests ``` -If you forget to set the service alias, the container image can't find the `dind` service, -and an error like the following is shown: +{{< /tab >}} -```plaintext -error during connect: Get http://docker:2376/v1.39/info: dial tcp: lookup docker on 192.168.0.1:53: no such host -``` +{{< tab title="With the Dependency Proxy" >}} -### Use a Docker-in-Docker container image with Dependency Proxy +In your `.gitlab-ci.yml` file: -You can use your own container images with Dependency Proxy. - -1. Set up [Docker-in-Docker](../../../ci/docker/using_docker_build.md#use-docker-in-docker). -1. Update the `image` and `service` to point to your registry. -1. Add a service [alias](../../../ci/services/_index.md#available-settings-for-services). +- Update `image` and `services` to point to your dependency proxy. +- Add a service [alias](../../../ci/services/_index.md#available-settings-for-services). Your `.gitlab-ci.yml` should look similar to this: @@ -107,6 +114,10 @@ build: - docker run my-docker-image /script/to/run/tests ``` +{{< /tab >}} + +{{< /tabs >}} + If you forget to set the service alias, the container image can't find the `dind` service, and an error like the following is shown: @@ -116,7 +127,7 @@ error during connect: Get http://docker:2376/v1.39/info: dial tcp: lookup docker ## Container registry examples with GitLab CI/CD -If you're using Docker-in-Docker on your runners, your `.gitlab-ci.yml` file should look similar to this: +If you're using DinD on your runners, your `.gitlab-ci.yml` file should look similar to this: ```yaml build: diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt index 2702700b436..db4ae910bb0 100644 --- a/scripts/frontend/quarantined_vue3_specs.txt +++ b/scripts/frontend/quarantined_vue3_specs.txt @@ -86,7 +86,6 @@ spec/frontend/jira_import/components/jira_import_form_spec.js spec/frontend/lib/utils/breadcrumbs_spec.js spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js spec/frontend/members/components/app_spec.js -spec/frontend/members/components/modals/leave_modal_spec.js spec/frontend/members/components/table/max_role_spec.js spec/frontend/members/components/table/members_table_spec.js spec/frontend/ml/model_registry/components/model_edit_spec.js @@ -110,7 +109,6 @@ spec/frontend/super_sidebar/components/user_menu_spec.js spec/frontend/tooltips/index_spec.js spec/frontend/vue_alerts_spec.js spec/frontend/vue_popovers_spec.js -spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js spec/frontend/vue_shared/components/file_tree_spec.js spec/frontend/vue_shared/components/filtered_search_bar/tokens/date_token_spec.js spec/frontend/vue_shared/components/metric_images/metric_image_details_modal_spec.js diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js index 3b2b8cee08f..ebb5057ed02 100644 --- a/spec/frontend/members/components/modals/leave_modal_spec.js +++ b/spec/frontend/members/components/modals/leave_modal_spec.js @@ -180,7 +180,7 @@ describe('LeaveModal', () => { const submitSpy = jest.spyOn(findForm().element, 'submit'); - findModal().findByText('Leave').trigger('click'); + findModal().vm.$emit('primary'); expect(submitSpy).toHaveBeenCalled(); diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js index ca9c2b7d381..156846e1873 100644 --- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js +++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { assertProps } from 'helpers/assert_props'; import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue'; describe('Design note pin component', () => { @@ -61,12 +62,9 @@ describe('Design note pin component', () => { }); it('throws when passed any other value except `sm` or `md`', () => { - jest.spyOn(console, 'error').mockImplementation(() => {}); - - createComponent({ size: 'lg' }); - - // eslint-disable-next-line no-console - expect(console.error).toHaveBeenCalled(); + expect(() => { + assertProps(DesignNotePin, { size: 'lg' }); + }).toThrow('Invalid prop: custom validator check failed'); }); }); diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 13d9f823636..e8c4f6a6453 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -61,10 +61,23 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do context 'when the user has permission to create notes on the discussion' do let(:discussion) { create(:discussion_note, project: project).to_discussion } - it 'creates a Note in a discussion' do - post_graphql_mutation(mutation, current_user: current_user) + context 'when discussion is not on the noteable' do + it 'checks noteable and discussion noteable' do + expect(noteable.id).not_to eq(discussion.noteable_id) + end - expect(mutation_response['note']['discussion']).to match a_graphql_entity_for(discussion) + it_behaves_like 'a mutation that returns top-level errors', + errors: ["The discussion does not exist or you don't have permission to perform this action"] + end + + context 'when the discussion is on the noteable' do + let(:noteable) { discussion.noteable } + + it 'creates a Note in a discussion' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['note']['discussion']).to match a_graphql_entity_for(discussion) + end end context 'when the discussion_id is not for a Discussion' do diff --git a/spec/services/work_items/data_sync/handlers/notes/copy_service_spec.rb b/spec/services/work_items/data_sync/handlers/notes/copy_service_spec.rb index c7f4291bdca..7846cb547d7 100644 --- a/spec/services/work_items/data_sync/handlers/notes/copy_service_spec.rb +++ b/spec/services/work_items/data_sync/handlers/notes/copy_service_spec.rb @@ -63,7 +63,7 @@ RSpec.describe WorkItems::DataSync::Handlers::Notes::CopyService, feature_catego # 4 notes are copied to the target work item: 2 system notes and 2 user notes # 2 system notes had also description version metadata - # 2 user notes notes had also description version metadata + # 2 user notes had also description version metadata expect { execute_service }.to change { ::Note.count }.by(5).and( change { ::SystemNoteMetadata.count }.by(2)).and( change { ::DescriptionVersion.count }.by(1)).and( @@ -71,7 +71,10 @@ RSpec.describe WorkItems::DataSync::Handlers::Notes::CopyService, feature_catego change { ::IssueUserMention.count }.by(2)) notes_details = target_work_item.reload.notes.pluck(:note, :discussion_id) - expect(notes_details).to match_array(expected_notes_details) + # same number of discussions in target and source work items + expect(notes_details.size).to eq(expected_notes_details.size) + # but with different discussion ids + expect(notes_details).not_to match_array(expected_notes_details) end it 'sets correct attributes from target', :aggregate_failures do @@ -82,5 +85,100 @@ RSpec.describe WorkItems::DataSync::Handlers::Notes::CopyService, feature_catego expect(expected_description_version.namespace_id).to eq(target_work_item.namespace_id) expect(expected_description_version.issue_id).to eq(target_work_item.id) end + + describe 'discussion_id generation' do + it 'generates new discussion_ids for copied notes' do + original_discussion_ids = work_item.notes.pluck(:discussion_id).uniq + + execute_service + + copied_discussion_ids = target_work_item.reload.notes.pluck(:discussion_id).uniq + + # All copied notes should have different discussion_ids from originals + expect(copied_discussion_ids).not_to include(*original_discussion_ids) + # Each copied discussion_id should be a valid 40-character hex string + copied_discussion_ids.all? { |discussion_id| expect(discussion_id).to match(/\A\h{40}\z/) } + end + + it 'reuses the same new discussion_id for notes in the same discussion' do + # Create notes that are part of the same discussion + discussion_note = create(:note, project: work_item.project, noteable: work_item) + reply_note = create(:note, + project: work_item.project, + noteable: work_item, + discussion_id: discussion_note.discussion_id, + in_reply_to: discussion_note + ) + + execute_service + + copied_notes = target_work_item.reload.notes.where(note: [discussion_note.note, reply_note.note]) + copied_discussion_ids = copied_notes.pluck(:discussion_id).uniq + + # Both copied notes should have the same new discussion_id + expect(copied_discussion_ids.size).to eq(1) + # But it should be different from the original + expect(copied_discussion_ids.first).not_to eq(discussion_note.discussion_id) + end + + it 'generates different discussion_ids for different original discussions' do + # Create two separate discussions + discussion1_note = create(:note, project: work_item.project, noteable: work_item) + discussion2_note = create(:note, project: work_item.project, noteable: work_item) + + execute_service + + copied_notes = target_work_item.reload.notes.where(note: [discussion1_note.note, discussion2_note.note]) + copied_discussion_ids = copied_notes.pluck(:discussion_id) + + # Each copied note should have a different discussion_id + expect(copied_discussion_ids.uniq.size).to eq(2) + expect(copied_discussion_ids).not_to include(discussion1_note.discussion_id, discussion2_note.discussion_id) + end + + it 'calls Discussion.discussion_id to generate new discussion_ids' do + expect(::Discussion).to receive(:discussion_id).at_least(:once).and_call_original + + execute_service + end + + it 'maintains discussion structure when copying notes with replies' do + # Create a discussion with multiple replies + parent_note = create(:note, project: work_item.project, noteable: work_item, note: 'Parent note') + create(:note, + project: work_item.project, + noteable: work_item, + note: 'Reply 1', + discussion_id: parent_note.discussion_id, + in_reply_to: parent_note + ) + create(:note, + project: work_item.project, + noteable: work_item, + note: 'Reply 2', + discussion_id: parent_note.discussion_id, + in_reply_to: parent_note + ) + + execute_service + + # Find the copied notes + copied_parent = target_work_item.reload.notes.find_by(note: 'Parent note') + copied_reply1 = target_work_item.notes.find_by(note: 'Reply 1') + copied_reply2 = target_work_item.notes.find_by(note: 'Reply 2') + + # All copied notes should have the same new discussion_id + expect(copied_parent.discussion_id).to eq(copied_reply1.discussion_id) + expect(copied_parent.discussion_id).to eq(copied_reply2.discussion_id) + + # But different from the original + expect(copied_parent.discussion_id).not_to eq(parent_note.discussion_id) + + # Verify the discussion structure is maintained + discussion = target_work_item.notes.find_discussion(copied_parent.discussion_id) + expect(discussion.notes.size).to eq(3) + expect(discussion.notes.map(&:note)).to contain_exactly('Parent note', 'Reply 1', 'Reply 2') + end + end end end diff --git a/spec/services/work_items/data_sync/widgets/notes_spec.rb b/spec/services/work_items/data_sync/widgets/notes_spec.rb index a3b67d46319..6f3c9957c22 100644 --- a/spec/services/work_items/data_sync/widgets/notes_spec.rb +++ b/spec/services/work_items/data_sync/widgets/notes_spec.rb @@ -105,7 +105,7 @@ RSpec.describe WorkItems::DataSync::Widgets::Notes, feature_category: :team_plan # 4 notes are copied to the target work item: 2 system notes and 2 user notes # 2 system notes had also description version metadata - # 2 user notes notes had also description version metadata + # 2 user notes had also description version metadata expect { callback.after_create }.to change { ::Note.count }.by(5).and( change { ::SystemNoteMetadata.count }.by(2)).and( change { ::DescriptionVersion.count }.by(1)).and( @@ -116,7 +116,11 @@ RSpec.describe WorkItems::DataSync::Widgets::Notes, feature_category: :team_plan change { ::IssueUserMention.count }.by(2)) notes_details = target_work_item.reload.notes.pluck(:note, :discussion_id) - expect(notes_details).to match_array(expected_notes_details) + # size and notes would match + expect(notes_details.size).to eq(expected_notes_details.size) + expect(notes_details.map(&:first)).to match_array(expected_notes_details.map(&:first)) + # but discussion_ids would not + expect(notes_details.map(&:last)).not_to match_array(expected_notes_details.map(&:last)) end end diff --git a/tooling/audit_events/docs/templates/audit_event_types.md.erb b/tooling/audit_events/docs/templates/audit_event_types.md.erb index ca4a0fa7709..61f3aac5f6f 100644 --- a/tooling/audit_events/docs/templates/audit_event_types.md.erb +++ b/tooling/audit_events/docs/templates/audit_event_types.md.erb @@ -9,7 +9,7 @@ <% def humanize(feature_category) %> <% case feature_category %> <% when 'duo_workflow' %> -<% "GitLab Duo Workflow" %> +<% "GitLab Duo Agent Platform" %> <% when 'mlops' %> <% "MLOps" %> <% when 'not_owned' %>