diff --git a/.gitlab/issue_templates/Implementation.md b/.gitlab/issue_templates/Implementation.md
index 058834264b0..f57bfae4da3 100644
--- a/.gitlab/issue_templates/Implementation.md
+++ b/.gitlab/issue_templates/Implementation.md
@@ -59,5 +59,17 @@ Other settings you might want to include when creating the issue.
# /epic &
-->
+## Verification steps
+
+
/label ~"workflow::refinement"
/milestone %Backlog
diff --git a/app/graphql/types/ci/job_kind_enum.rb b/app/graphql/types/ci/job_kind_enum.rb
new file mode 100644
index 00000000000..dd1d80f806c
--- /dev/null
+++ b/app/graphql/types/ci/job_kind_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class JobKindEnum < BaseEnum
+ graphql_name 'CiJobKind'
+
+ value 'BUILD', value: ::Ci::Build, description: 'Standard CI job.'
+ value 'BRIDGE', value: ::Ci::Bridge, description: 'Bridge CI job connecting a parent and child pipeline.'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 83054553bd8..f25fc56a588 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -17,6 +17,8 @@ module Types
description: 'Duration of the job in seconds.'
field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true,
description: 'ID of the job.'
+ field :kind, type: ::Types::Ci::JobKindEnum, null: false,
+ description: 'Indicates the type of job.'
field :name, GraphQL::Types::String, null: true,
description: 'Name of the job.'
field :needs, BuildNeedType.connection_type, null: true,
@@ -87,6 +89,12 @@ module Types
field :triggered, GraphQL::Types::Boolean, null: true,
description: 'Whether the job was triggered.'
+ def kind
+ return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class)
+
+ object.class
+ end
+
def pipeline
Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, object.pipeline_id).find
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 044404562af..b4ad9db815d 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -134,14 +134,14 @@ module SortingHelper
)
end
- def milestone_sort_options_hash
+ def milestones_sort_options_hash
{
- sort_value_name => sort_title_name_asc,
- sort_value_name_desc => sort_title_name_desc,
- sort_value_due_date_later => sort_title_due_date_later,
sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_due_date_later => sort_title_due_date_later,
+ sort_value_start_date_soon => sort_title_start_date_soon,
sort_value_start_date_later => sort_title_start_date_later,
- sort_value_start_date_soon => sort_title_start_date_soon
+ sort_value_name => sort_title_name_asc,
+ sort_value_name_desc => sort_title_name_desc
}
end
diff --git a/app/mailers/emails/reviews.rb b/app/mailers/emails/reviews.rb
index ddb9e161a80..b98fa8aa6c9 100644
--- a/app/mailers/emails/reviews.rb
+++ b/app/mailers/emails/reviews.rb
@@ -22,6 +22,10 @@ module Emails
review = Review.find_by_id(review_id)
@notes = review.notes
+ @discussions = Discussion.build_discussions(review.discussion_ids, preload_note_diff_file: true)
+ @include_diff_discussion_stylesheet = @discussions.values.any? do |discussion|
+ discussion.diff_discussion? && discussion.on_text?
+ end
@author = review.author
@author_name = review.author_name
@project = review.project
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 8a167034629..9eb3308b901 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -47,6 +47,14 @@ class Discussion
grouped_notes.values.map { |notes| build(notes, context_noteable) }
end
+ def self.build_discussions(discussion_ids, context_noteable = nil, preload_note_diff_file: false)
+ notes = Note.where(discussion_id: discussion_ids).fresh
+ notes = notes.inc_note_diff_file if preload_note_diff_file
+
+ grouped_notes = notes.group_by { |n| n.discussion_id }
+ grouped_notes.transform_values { |notes| Discussion.build(notes, context_noteable) }
+ end
+
def self.lazy_find(discussion_id)
BatchLoader.for(discussion_id).batch do |discussion_ids, loader|
results = Note.where(discussion_id: discussion_ids).fresh.to_a.group_by(&:discussion_id)
diff --git a/app/models/note.rb b/app/models/note.rb
index 4f2e7ebe2c5..8f518735442 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -121,6 +121,7 @@ class Note < ApplicationRecord
scope :with_discussion_ids, ->(discussion_ids) { where(discussion_id: discussion_ids) }
scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author, -> { includes(:author) }
+ scope :inc_note_diff_file, -> { includes(:note_diff_file) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
scope :inc_relations_for_view, -> do
includes({ project: :group }, { author: :status }, :updated_by, :resolved_by, :award_emoji,
diff --git a/app/models/review.rb b/app/models/review.rb
index 5a30e2963c8..c621da3b03c 100644
--- a/app/models/review.rb
+++ b/app/models/review.rb
@@ -14,6 +14,10 @@ class Review < ApplicationRecord
participant :author
+ def discussion_ids
+ notes.select(:discussion_id)
+ end
+
def all_references(current_user = nil, extractor: nil)
ext = super
diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml
index d5398337320..f2c67b84c80 100644
--- a/app/views/notify/_note_email.html.haml
+++ b/app/views/notify/_note_email.html.haml
@@ -2,17 +2,18 @@
- diff_limit = local_assigns.fetch(:diff_limit, nil)
- target_url = local_assigns.fetch(:target_url, @target_url)
- note_style = local_assigns.fetch(:note_style, "")
-- skip_stylesheet_link = local_assigns.fetch(:skip_stylesheet_link, false)
+- include_stylesheet_link = local_assigns.fetch(:include_stylesheet_link, true)
-- discussion = note.discussion if note.part_of_discussion?
+- author = local_assigns.fetch(:author) { note.author }
+- discussion = local_assigns.fetch(:discussion) { note.discussion } if note.part_of_discussion?
%p{ style: "color: #777777;" }
= succeed ':' do
- = link_to note.author_name, user_url(note.author)
+ = link_to author.name, user_url(author)
- if discussion.nil?
= link_to 'commented', target_url
- else
- - if note.start_of_discussion?
+ - if discussion.first_note == note
started a new
- else
commented on a
@@ -22,10 +23,9 @@
- else
= link_to 'discussion', target_url
-- if discussion&.diff_discussion? && discussion.on_text?
- - unless skip_stylesheet_link
- = content_for :head do
- = stylesheet_link_tag 'mailers/highlighted_diff_email'
+- if include_stylesheet_link && discussion&.diff_discussion? && discussion.on_text?
+ = content_for :head do
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
%table.code.gl-mb-5
= render partial: "projects/diffs/email_line",
@@ -34,4 +34,4 @@
locals: { diff_file: discussion.diff_file }
.md{ style: note_style }
- = markdown(note.note, pipeline: :email, author: note.author, current_user: @recipient, issuable_reference_expansion_enabled: true)
+ = markdown(note.note, pipeline: :email, author: author, current_user: @recipient, issuable_reference_expansion_enabled: true)
diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb
index 8e2f7e6f76e..8853519fb8d 100644
--- a/app/views/notify/_note_email.text.erb
+++ b/app/views/notify/_note_email.text.erb
@@ -1,13 +1,14 @@
<% note = local_assigns.fetch(:note, @note) -%>
<% diff_limit = local_assigns.fetch(:diff_limit, nil) -%>
<% target_url = local_assigns.fetch(:target_url, @target_url) -%>
-<% discussion = note.discussion if note.part_of_discussion? -%>
+<% author = local_assigns.fetch(:author) { note.author } -%>
+<% discussion = local_assigns.fetch(:discussion) { note.discussion } if note.part_of_discussion? -%>
-<%= sanitize_name(note.author_name) -%>
+<%= sanitize_name(author.name) -%>
<% if discussion.nil? -%>
<%= 'commented' -%>:
<% else -%>
-<% if note.start_of_discussion? -%>
+<% if discussion.first_note == note -%>
<%= 'started a new discussion' -%>
<% else -%>
<%= 'commented on a discussion' -%>
diff --git a/app/views/notify/new_review_email.html.haml b/app/views/notify/new_review_email.html.haml
index d5399c872d5..fdabc204596 100644
--- a/app/views/notify/new_review_email.html.haml
+++ b/app/views/notify/new_review_email.html.haml
@@ -1,5 +1,6 @@
-= content_for :head do
- = stylesheet_link_tag 'mailers/highlighted_diff_email'
+- if @include_diff_discussion_stylesheet
+ = content_for :head do
+ = stylesheet_link_tag 'mailers/highlighted_diff_email'
%table{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;margin:0 auto;border-collapse:separate;border-spacing:0;" }
%tbody
@@ -15,5 +16,9 @@
%tr
%td{ style: "overflow:hidden;font-size:14px;line-height:1.4;display:grid;" }
- @notes.each do |note|
+ -# Get preloaded note discussion
+ - discussion = @discussions[note.discussion_id] if note.part_of_discussion?
+ -# Preload project for discussions first note
+ - discussion.first_note.project = @project if discussion&.first_note
- target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}")
- = render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:1px solid #ededed;", skip_stylesheet_link: true
+ = render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:1px solid #ededed;", include_stylesheet_link: false, discussion: discussion, author: @author
diff --git a/app/views/notify/new_review_email.text.erb b/app/views/notify/new_review_email.text.erb
index 164735abad0..7bf878aefd0 100644
--- a/app/views/notify/new_review_email.text.erb
+++ b/app/views/notify/new_review_email.text.erb
@@ -4,8 +4,10 @@
--
<% @notes.each_with_index do |note, index| %>
+
+ <% discussion = @discussions[note.discussion_id] if note.part_of_discussion?%>
<% target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}") %>
- <%= render 'note_email', note: note, diff_limit: 3, target_url: target_url %>
+ <%= render 'note_email', note: note, diff_limit: 3, target_url: target_url, discussion: discussion, author: @author %>
<% if index != @notes.length-1 %>
--
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index 29c01343358..1c6eb7aa96b 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -1,22 +1,4 @@
-.dropdown.inline.gl-ml-3
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- %span.light
- - if @sort.present?
- = milestone_sort_options_hash[@sort]
- - else
- = sort_title_due_date_soon
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
- %li
- = link_to page_filter_path(sort: sort_value_due_date_soon) do
- = sort_title_due_date_soon
- = link_to page_filter_path(sort: sort_value_due_date_later) do
- = sort_title_due_date_later
- = link_to page_filter_path(sort: sort_value_start_date_soon) do
- = sort_title_start_date_soon
- = link_to page_filter_path(sort: sort_value_start_date_later) do
- = sort_title_start_date_later
- = link_to page_filter_path(sort: sort_value_name) do
- = sort_title_name_asc
- = link_to page_filter_path(sort: sort_value_name_desc) do
- = sort_title_name_desc
+- milestones_sort_options = milestones_sort_options_hash.map { |value, text| { value: value, text: text, href: page_filter_path(sort: value) } }
+
+%div{ data: {testid: 'milestone_sort_by_dropdown'} }
+ = gl_redirect_listbox_tag milestones_sort_options, @sort, class: 'gl-ml-3'
diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml
index 2c591c78585..8f228120c8e 100644
--- a/config/gitlab_loose_foreign_keys.yml
+++ b/config/gitlab_loose_foreign_keys.yml
@@ -207,10 +207,6 @@ pages_deployments:
- table: ci_builds
column: ci_build_id
on_delete: async_nullify
-project_pages_metadata:
- - table: ci_job_artifacts
- column: artifacts_archive_id
- on_delete: async_nullify
requirements_management_test_reports:
- table: ci_builds
column: build_id
diff --git a/db/post_migrate/20220324110247_untrack_deletions_on_ci_job_artifacts.rb b/db/post_migrate/20220324110247_untrack_deletions_on_ci_job_artifacts.rb
new file mode 100644
index 00000000000..d178c235e21
--- /dev/null
+++ b/db/post_migrate/20220324110247_untrack_deletions_on_ci_job_artifacts.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class UntrackDeletionsOnCiJobArtifacts < Gitlab::Database::Migration[1.0]
+ include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers
+
+ enable_lock_retries!
+
+ def up
+ untrack_record_deletions(:ci_job_artifacts)
+ end
+
+ def down
+ track_record_deletions(:ci_job_artifacts)
+ end
+end
diff --git a/db/post_migrate/20220324180717_remove_project_pages_metadata_artifacts_archive_id_column.rb b/db/post_migrate/20220324180717_remove_project_pages_metadata_artifacts_archive_id_column.rb
new file mode 100644
index 00000000000..287752b6b5c
--- /dev/null
+++ b/db/post_migrate/20220324180717_remove_project_pages_metadata_artifacts_archive_id_column.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveProjectPagesMetadataArtifactsArchiveIdColumn < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ remove_column :project_pages_metadata, :artifacts_archive_id
+ end
+
+ def down
+ unless column_exists?(:project_pages_metadata, :artifacts_archive_id)
+ add_column :project_pages_metadata, :artifacts_archive_id, :bigint
+ end
+
+ add_concurrent_index(
+ :project_pages_metadata,
+ :artifacts_archive_id,
+ name: "index_project_pages_metadata_on_artifacts_archive_id"
+ )
+ end
+end
diff --git a/db/post_migrate/20220329175119_remove_leftover_ci_job_artifact_deletions.rb b/db/post_migrate/20220329175119_remove_leftover_ci_job_artifact_deletions.rb
new file mode 100644
index 00000000000..f0c09598bfb
--- /dev/null
+++ b/db/post_migrate/20220329175119_remove_leftover_ci_job_artifact_deletions.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class RemoveLeftoverCiJobArtifactDeletions < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ # Delete all pending record deletions in the public.ci_job_artifacts until
+ # there are no more rows left.
+ loop do
+ result = execute <<~SQL
+ DELETE FROM "loose_foreign_keys_deleted_records"
+ WHERE
+ ("loose_foreign_keys_deleted_records"."partition", "loose_foreign_keys_deleted_records"."id") IN (
+ SELECT "loose_foreign_keys_deleted_records"."partition", "loose_foreign_keys_deleted_records"."id"
+ FROM "loose_foreign_keys_deleted_records"
+ WHERE
+ "loose_foreign_keys_deleted_records"."fully_qualified_table_name" = 'public.ci_job_artifacts' AND
+ "loose_foreign_keys_deleted_records"."status" = 1
+ LIMIT 100
+ )
+ SQL
+
+ break if result.cmd_tuples == 0
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20220324110247 b/db/schema_migrations/20220324110247
new file mode 100644
index 00000000000..0bfc9ff4e4e
--- /dev/null
+++ b/db/schema_migrations/20220324110247
@@ -0,0 +1 @@
+35aaf03898795800027c814a6f907af6f011bd5095cae7188234b46f4b2ebb90
\ No newline at end of file
diff --git a/db/schema_migrations/20220324180717 b/db/schema_migrations/20220324180717
new file mode 100644
index 00000000000..b9bac081bcc
--- /dev/null
+++ b/db/schema_migrations/20220324180717
@@ -0,0 +1 @@
+56706c8d4139c63427838d37b7120c57fd39c7be4caee0ebba39a05cd3c453f7
\ No newline at end of file
diff --git a/db/schema_migrations/20220329175119 b/db/schema_migrations/20220329175119
new file mode 100644
index 00000000000..5e0c0306594
--- /dev/null
+++ b/db/schema_migrations/20220329175119
@@ -0,0 +1 @@
+ec5dfe48e13cdbce5fc0c67612cddf1a5b555b3c84e1869ee3a7f047f0f3f1a0
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 45f0fae3389..5bbfb65771a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19278,7 +19278,6 @@ ALTER SEQUENCE project_mirror_data_id_seq OWNED BY project_mirror_data.id;
CREATE TABLE project_pages_metadata (
project_id bigint NOT NULL,
deployed boolean DEFAULT false NOT NULL,
- artifacts_archive_id bigint,
pages_deployment_id bigint
);
@@ -28666,8 +28665,6 @@ CREATE UNIQUE INDEX index_project_mirror_data_on_project_id ON project_mirror_da
CREATE INDEX index_project_mirror_data_on_status ON project_mirror_data USING btree (status);
-CREATE INDEX index_project_pages_metadata_on_artifacts_archive_id ON project_pages_metadata USING btree (artifacts_archive_id);
-
CREATE INDEX index_project_pages_metadata_on_pages_deployment_id ON project_pages_metadata USING btree (pages_deployment_id);
CREATE INDEX index_project_pages_metadata_on_project_id_and_deployed_is_true ON project_pages_metadata USING btree (project_id) WHERE (deployed = true);
@@ -30968,8 +30965,6 @@ CREATE TRIGGER chat_names_loose_fk_trigger AFTER DELETE ON chat_names REFERENCIN
CREATE TRIGGER ci_builds_loose_fk_trigger AFTER DELETE ON ci_builds REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
-CREATE TRIGGER ci_job_artifacts_loose_fk_trigger AFTER DELETE ON ci_job_artifacts REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
-
CREATE TRIGGER ci_pipelines_loose_fk_trigger AFTER DELETE ON ci_pipelines REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
CREATE TRIGGER ci_runners_loose_fk_trigger AFTER DELETE ON ci_runners REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 4a09f9a6605..c2e2b7f87ba 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -282,7 +282,8 @@ Deployments created by users on GitLab Premium or higher include the `approvals`
"web_url": "http://localhost:3000/project_6_bot"
},
"status": "approved",
- "created_at": "2022-02-24T20:22:30.097Z"
+ "created_at": "2022-02-24T20:22:30.097Z",
+ "comment": "Looks good to me"
}
],
...
@@ -342,20 +343,7 @@ Deployments created by users on GitLab Premium or higher include the `approvals`
{
"status": "created",
"pending_approval_count": 0,
- "approvals": [
- {
- "user": {
- "id": 49,
- "username": "project_6_bot",
- "name": "****",
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e83ac685f68ea07553ad3054c738c709?s=80&d=identicon",
- "web_url": "http://localhost:3000/project_6_bot"
- },
- "status": "approved",
- "created_at": "2022-02-24T20:22:30.097Z"
- }
- ],
+ "approvals": [],
...
}
```
@@ -420,7 +408,8 @@ Deployments created by users on GitLab Premium or higher include the `approvals`
"web_url": "http://localhost:3000/project_6_bot"
},
"status": "approved",
- "created_at": "2022-02-24T20:22:30.097Z"
+ "created_at": "2022-02-24T20:22:30.097Z",
+ "comment": "Looks good to me"
}
],
...
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 098788ee810..cee149b3f9f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -9353,6 +9353,7 @@ Represents the total number of issues and their weights for a particular day.
| `duration` | [`Int`](#int) | Duration of the job in seconds. |
| `finishedAt` | [`Time`](#time) | When a job has finished running. |
| `id` | [`JobID`](#jobid) | ID of the job. |
+| `kind` | [`CiJobKind!`](#cijobkind) | Indicates the type of job. |
| `manualJob` | [`Boolean`](#boolean) | Whether the job has a manual action. |
| `name` | [`String`](#string) | Name of the job. |
| `needs` | [`CiBuildNeedConnection`](#cibuildneedconnection) | References to builds that must complete before the jobs run. (see [Connections](#connections)) |
@@ -17772,6 +17773,13 @@ Values for YAML processor result.
| `INVALID` | Configuration file is not valid. |
| `VALID` | Configuration file is valid. |
+### `CiJobKind`
+
+| Value | Description |
+| ----- | ----------- |
+| `BRIDGE` | Bridge CI job connecting a parent and child pipeline. |
+| `BUILD` | Standard CI job. |
+
### `CiJobStatus`
| Value | Description |
diff --git a/doc/api/members.md b/doc/api/members.md
index 10072baf2ff..1daa8acb794 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -63,6 +63,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-09-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
"group_saml_identity": null,
@@ -75,6 +84,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-09-22T14:13:35Z",
+ "created_by": {
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
"email": "john@example.com",
@@ -132,6 +150,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-09-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
"group_saml_identity": null,
@@ -144,6 +171,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-09-22T14:13:35Z",
+ "created_by": {
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
"email": "john@example.com",
@@ -161,6 +197,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-11-22T14:13:35Z",
"access_level": 30,
"group_saml_identity": null,
@@ -201,6 +246,14 @@ Example response:
"access_level": 30,
"email": "john@example.com",
"created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": null,
"group_saml_identity": null,
"membership_state": "active"
@@ -239,6 +292,15 @@ Example response:
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
"access_level": 30,
+ "created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"email": "john@example.com",
"expires_at": null,
"group_saml_identity": null,
@@ -454,6 +516,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 30,
"email": "john@example.com",
@@ -492,6 +563,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 40,
"email": "john@example.com",
@@ -529,6 +609,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22T14:13:35Z",
"access_level": 40,
"email": "john@example.com",
@@ -566,6 +655,15 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-10-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root"
+ },
"expires_at": "2012-10-22",
"access_level": 40,
"email": "john@example.com",
diff --git a/doc/ci/index.md b/doc/ci/index.md
index 54ce8b7f299..05e97613384 100644
--- a/doc/ci/index.md
+++ b/doc/ci/index.md
@@ -76,36 +76,36 @@ Certain operations can only be performed according to the
GitLab CI/CD features, grouped by DevOps stage, include:
-| Feature | Description |
-|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------|
-| **Configure** | |
-| [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. |
-| [ChatOps](chatops/index.md) | Trigger CI jobs from chat, with results sent back to the channel. |
-| [Connect to cloud services](cloud_services/index.md) | Connect to cloud providers using OpenID Connect (OIDC) to retrieve temporary credentials to access services or secrets. |
-| **Verify** | |
-| [Browser Performance Testing](../user/project/merge_requests/browser_performance_testing.md) | Quickly determine the browser performance impact of pending code changes. |
-| [Load Performance Testing](../user/project/merge_requests/load_performance_testing.md) | Quickly determine the server performance impact of pending code changes. |
-| [CI services](services/index.md) | Link Docker containers with your base image. |
-| [GitLab CI/CD for external repositories](ci_cd_for_external_repos/index.md) | Get the benefits of GitLab CI/CD combined with repositories in GitHub and Bitbucket Cloud. |
-| [Interactive Web Terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
-| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
-| [Unit test reports](unit_test_reports.md) | Identify test failures directly on merge requests. |
-| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
-| **Release** | |
-| [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
-| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
-| [Canary Deployments](../user/project/canary_deployments.md) | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
-| [Deploy boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
-| [Feature Flags](../operations/feature_flags.md) | Deploy your features behind Feature Flags. |
-| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
-| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
-| [Cloud deployment](cloud_deployment/index.md) | Deploy your application to a main cloud provider. |
-| **Secure** | |
-| [Code Quality](../user/project/merge_requests/code_quality.md) | Analyze your source code quality. |
-| [Container Scanning](../user/application_security/container_scanning/index.md) | Check your Docker containers for known vulnerabilities. |
-| [Dependency Scanning](../user/application_security/dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
-| [License Compliance](../user/compliance/license_compliance/index.md) | Search your project dependencies for their licenses. |
-| [Security Test reports](../user/application_security/index.md) | Check for app vulnerabilities. |
+| Feature | Description |
+|:---------------------------------------------------------------------------------------------|:------------|
+| **Configure** | |
+| [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. |
+| [ChatOps](chatops/index.md) | Trigger CI jobs from chat, with results sent back to the channel. |
+| [Connect to cloud services](cloud_services/index.md) | Connect to cloud providers using OpenID Connect (OIDC) to retrieve temporary credentials to access services or secrets. |
+| **Verify** | |
+| [Browser Performance Testing](../user/project/merge_requests/browser_performance_testing.md) | Quickly determine the browser performance impact of pending code changes. |
+| [Load Performance Testing](../user/project/merge_requests/load_performance_testing.md) | Quickly determine the server performance impact of pending code changes. |
+| [CI services](services/index.md) | Link Docker containers with your base image. |
+| [GitLab CI/CD for external repositories](ci_cd_for_external_repos/index.md) | Get the benefits of GitLab CI/CD combined with repositories in GitHub and Bitbucket Cloud. |
+| [Interactive Web Terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
+| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
+| [Unit test reports](unit_test_reports.md) | Identify test failures directly on merge requests. |
+| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
+| **Release** | |
+| [Auto Deploy](../topics/autodevops/stages.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
+| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
+| [Canary Deployments](../user/project/canary_deployments.md) | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
+| [Deploy boards](../user/project/deploy_boards.md) | Check the current health and status of each CI/CD environment running on Kubernetes. |
+| [Feature Flags](../operations/feature_flags.md) | Deploy your features behind Feature Flags. |
+| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
+| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
+| [Cloud deployment](cloud_deployment/index.md) | Deploy your application to a main cloud provider. |
+| **Secure** | |
+| [Code Quality](../user/project/merge_requests/code_quality.md) | Analyze your source code quality. |
+| [Container Scanning](../user/application_security/container_scanning/index.md) | Check your Docker containers for known vulnerabilities. |
+| [Dependency Scanning](../user/application_security/dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
+| [License Compliance](../user/compliance/license_compliance/index.md) | Search your project dependencies for their licenses. |
+| [Security Test reports](../user/application_security/index.md) | Check for app vulnerabilities. |
## Examples
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 11f623641c1..8d5f7104301 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -120,7 +120,7 @@ the following table) as these were used for development and testing:
| GitLab version | Minimum PostgreSQL version |
|----------------|----------------------------|
| 13.0 | 11 |
-| 14.0 | 12 |
+| 14.0 | 12.10 |
You must also ensure the following extensions are loaded into every
GitLab database. [Read more about this requirement, and troubleshooting](postgresql_extensions.md).
diff --git a/doc/update/index.md b/doc/update/index.md
index b9951f3997e..cb0c7b63072 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -398,6 +398,8 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
### 14.8.0
+- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, please review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
+ Updating to 14.8.2 or later will reset runner registration tokens for your groups and projects.
- The agent server for Kubernetes [is enabled by default](https://about.gitlab.com/releases/2022/02/22/gitlab-14-8-released/#the-agent-server-for-kubernetes-is-enabled-by-default)
on Omnibus installations. If you run GitLab at scale,
such as [the reference architectures](../administration/reference_architectures/index.md),
@@ -421,11 +423,15 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
### 14.7.0
- See [LFS objects import and mirror issue in GitLab 14.6.0 to 14.7.2](#lfs-objects-import-and-mirror-issue-in-gitlab-1460-to-1472).
+- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, please review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
+ Updating to 14.7.4 or later will reset runner registration tokens for your groups and projects.
### 14.6.0
- See [LFS objects import and mirror issue in GitLab 14.6.0 to 14.7.2](#lfs-objects-import-and-mirror-issue-in-gitlab-1460-to-1472).
-
+- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, please review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
+ Updating to 14.6.5 or later will reset runner registration tokens for your groups and projects.
+
### 14.5.0
- When `make` is run, Gitaly builds are now created in `_build/bin` and no longer in the root directory of the source directory. If you
diff --git a/lib/api/entities/member.rb b/lib/api/entities/member.rb
index 87f03adba31..7ce1e73a043 100644
--- a/lib/api/entities/member.rb
+++ b/lib/api/entities/member.rb
@@ -6,6 +6,7 @@ module API
expose :user, merge: true, using: UserBasic
expose :access_level
expose :created_at
+ expose :created_by, with: UserBasic, expose_nil: false
expose :expires_at
end
end
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index a06e64fdee0..22d7ff91d41 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Milestones sorting', :js do
sign_in(user)
end
- it 'visit group milestones and sort by due_date_asc' do
+ it 'visit group milestones and sort by due_date_asc', :js do
visit group_milestones_path(group)
expect(page).to have_button('Due soon')
@@ -27,13 +27,13 @@ RSpec.describe 'Milestones sorting', :js do
expect(page.all('ul.content-list > li strong > a').map(&:text)).to eq(['v2.0', 'v2.0', 'v3.0', 'v1.0', 'v1.0'])
end
- click_button 'Due soon'
+ within '[data-testid=milestone_sort_by_dropdown]' do
+ click_button 'Due soon'
+ expect(find('.gl-new-dropdown-contents').all('.gl-new-dropdown-item-text-wrapper p').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
- expect(find('ul.dropdown-menu-sort li').all('a').map(&:text)).to eq(['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending'])
-
- click_link 'Due later'
-
- expect(page).to have_button('Due later')
+ click_button 'Due later'
+ expect(page).to have_button('Due later')
+ end
# assert descending sorting
within '.milestones' do
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index 565c61cfaa0..2ad820e4a06 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -5,49 +5,55 @@ require 'spec_helper'
RSpec.describe 'Milestones sorting', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:milestones_for_sort_by) do
+ {
+ 'Due later' => %w[b c a],
+ 'Name, ascending' => %w[a b c],
+ 'Name, descending' => %w[c b a],
+ 'Start later' => %w[a c b],
+ 'Start soon' => %w[b c a],
+ 'Due soon' => %w[a c b]
+ }
+ end
+
+ let(:ordered_milestones) do
+ ['Due soon', 'Due later', 'Start soon', 'Start later', 'Name, ascending', 'Name, descending']
+ end
before do
- # Milestones
- create(:milestone,
- due_date: 10.days.from_now,
- created_at: 2.hours.ago,
- title: "aaa", project: project)
- create(:milestone,
- due_date: 11.days.from_now,
- created_at: 1.hour.ago,
- title: "bbb", project: project)
+ create(:milestone, start_date: 7.days.from_now, due_date: 10.days.from_now, title: "a", project: project)
+ create(:milestone, start_date: 6.days.from_now, due_date: 11.days.from_now, title: "c", project: project)
+ create(:milestone, start_date: 5.days.from_now, due_date: 12.days.from_now, title: "b", project: project)
sign_in(user)
end
- it 'visit project milestones and sort by due_date_asc' do
+ it 'visit project milestones and sort by various orders' do
visit project_milestones_path(project)
expect(page).to have_button('Due soon')
- # assert default sorting
+ # assert default sorting order
within '.milestones' do
- expect(page.all('ul.content-list > li').first.text).to include('aaa')
- expect(page.all('ul.content-list > li').last.text).to include('bbb')
+ expect(page.all('ul.content-list > li strong > a').map(&:text)).to eq(%w[a c b])
end
- click_button 'Due soon'
+ # assert milestones listed for given sort order
+ selected_sort_order = 'Due soon'
+ milestones_for_sort_by.each do |sort_by, expected_milestones|
+ within '[data-testid=milestone_sort_by_dropdown]' do
+ click_button selected_sort_order
+ milestones = find('.gl-new-dropdown-contents').all('.gl-new-dropdown-item-text-wrapper p').map(&:text)
+ expect(milestones).to eq(ordered_milestones)
- sort_options = find('ul.dropdown-menu-sort li').all('a').collect(&:text)
+ click_button sort_by
+ expect(page).to have_button(sort_by)
+ end
- expect(sort_options[0]).to eq('Due soon')
- expect(sort_options[1]).to eq('Due later')
- expect(sort_options[2]).to eq('Start soon')
- expect(sort_options[3]).to eq('Start later')
- expect(sort_options[4]).to eq('Name, ascending')
- expect(sort_options[5]).to eq('Name, descending')
+ within '.milestones' do
+ expect(page.all('ul.content-list > li strong > a').map(&:text)).to eq(expected_milestones)
+ end
- click_link 'Due later'
-
- expect(page).to have_button('Due later')
-
- within '.milestones' do
- expect(page.all('ul.content-list > li').first.text).to include('bbb')
- expect(page.all('ul.content-list > li').last.text).to include('aaa')
+ selected_sort_order = sort_by
end
end
end
diff --git a/spec/graphql/types/ci/job_kind_enum_spec.rb b/spec/graphql/types/ci/job_kind_enum_spec.rb
new file mode 100644
index 00000000000..b48d20b71e2
--- /dev/null
+++ b/spec/graphql/types/ci/job_kind_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiJobKind'] do
+ it 'exposes some job type values' do
+ expect(described_class.values.keys).to match_array(
+ (%w[BRIDGE BUILD])
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index 47d697ab8b8..655c3636883 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe Types::Ci::JobType do
downstreamPipeline
finished_at
id
+ kind
manual_job
name
needs
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e2ee63078bb..b6ad66f41b5 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -2181,18 +2181,46 @@ RSpec.describe Notify do
context 'when diff note' do
let!(:notes) { create_list(:diff_note_on_merge_request, 3, review: review, project: project, author: review.author, noteable: merge_request) }
- it 'links to notes' do
+ it 'links to notes and discussions', :aggregate_failures do
+ reply_note = create(:diff_note_on_merge_request, review: review, project: project, author: review.author, noteable: merge_request, in_reply_to: notes.first)
+
review.notes.each do |note|
# Text part
expect(subject.text_part.body.raw_source).to include(
project_merge_request_url(project, merge_request, anchor: "note_#{note.id}")
)
+
+ if note == reply_note
+ expect(subject.text_part.body.raw_source).to include("commented on a discussion on #{note.discussion.file_path}")
+ else
+ expect(subject.text_part.body.raw_source).to include("started a new discussion on #{note.discussion.file_path}")
+ end
end
end
it 'includes only one link to the highlighted_diff_email' do
expect(subject.html_part.body.raw_source).to include('assets/mailers/highlighted_diff_email').once
end
+
+ it 'avoids N+1 cached queries when rendering html', :use_sql_query_cache, :request_store do
+ control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true, skip_cached: false) do
+ subject.html_part
+ end
+
+ create_list(:diff_note_on_merge_request, 3, review: review, project: project, author: review.author, noteable: merge_request)
+
+ expect { described_class.new_review_email(recipient.id, review.id).html_part }.not_to exceed_all_query_limit(control_count)
+ end
+
+ it 'avoids N+1 cached queries when rendering text', :use_sql_query_cache, :request_store do
+ control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true, skip_cached: false) do
+ subject.text_part
+ end
+
+ create_list(:diff_note_on_merge_request, 3, review: review, project: project, author: review.author, noteable: merge_request)
+
+ expect { described_class.new_review_email(recipient.id, review.id).text_part }.not_to exceed_all_query_limit(control_count)
+ end
end
it 'contains review author name' do
diff --git a/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb b/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb
new file mode 100644
index 00000000000..13884007af2
--- /dev/null
+++ b/spec/migrations/20220329175119_remove_leftover_ci_job_artifact_deletions_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RemoveLeftoverCiJobArtifactDeletions do
+ let(:deleted_records) { table(:loose_foreign_keys_deleted_records) }
+
+ target_table_name = Ci::JobArtifact.table_name
+
+ let(:pending_record1) do
+ deleted_records.create!(
+ id: 1,
+ fully_qualified_table_name: "public.#{target_table_name}",
+ primary_key_value: 1,
+ status: 1
+ )
+ end
+
+ let(:pending_record2) do
+ deleted_records.create!(
+ id: 2,
+ fully_qualified_table_name: "public.#{target_table_name}",
+ primary_key_value: 2,
+ status: 1
+ )
+ end
+
+ let(:other_pending_record1) do
+ deleted_records.create!(
+ id: 3,
+ fully_qualified_table_name: 'public.projects',
+ primary_key_value: 1,
+ status: 1
+ )
+ end
+
+ let(:other_pending_record2) do
+ deleted_records.create!(
+ id: 4,
+ fully_qualified_table_name: 'public.ci_builds',
+ primary_key_value: 1,
+ status: 1
+ )
+ end
+
+ let(:processed_record1) do
+ deleted_records.create!(
+ id: 5,
+ fully_qualified_table_name: 'public.external_pull_requests',
+ primary_key_value: 3,
+ status: 2
+ )
+ end
+
+ let(:other_processed_record1) do
+ deleted_records.create!(
+ id: 6,
+ fully_qualified_table_name: 'public.ci_builds',
+ primary_key_value: 2,
+ status: 2
+ )
+ end
+
+ let!(:persisted_ids_before) do
+ [
+ pending_record1,
+ pending_record2,
+ other_pending_record1,
+ other_pending_record2,
+ processed_record1,
+ other_processed_record1
+ ].map(&:id).sort
+ end
+
+ let!(:persisted_ids_after) do
+ [
+ other_pending_record1,
+ other_pending_record2,
+ processed_record1,
+ other_processed_record1
+ ].map(&:id).sort
+ end
+
+ def all_ids
+ deleted_records.all.map(&:id).sort
+ end
+
+ it 'deletes pending external_pull_requests records' do
+ expect { migrate! }.to change { all_ids }.from(persisted_ids_before).to(persisted_ids_after)
+ end
+end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index bd0397e0396..2b046142b59 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -700,10 +700,6 @@ RSpec.describe Ci::JobArtifact do
MSG
end
- it_behaves_like 'it has loose foreign keys' do
- let(:factory_name) { :ci_job_artifact }
- end
-
context 'loose foreign key on ci_job_artifacts.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index b0514a0a963..ddb2664d353 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -52,6 +52,7 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
'name' => job_2.name,
'allowFailure' => job_2.allow_failure,
'duration' => 25,
+ 'kind' => 'BUILD',
'queuedDuration' => 2.0,
'status' => job_2.status.upcase
)
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index b191b585d06..2d1bb45390b 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -155,6 +155,56 @@ RSpec.describe 'Query.project.pipeline' do
end
end
+ describe '.jobs.kind' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ nodes {
+ groups{
+ nodes {
+ jobs {
+ nodes {
+ kind
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the job is a build' do
+ it 'returns BUILD' do
+ create(:ci_build, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ job_data = graphql_data_at(:project, :pipeline, :stages, :nodes, :groups, :nodes, :jobs, :nodes).first
+ expect(job_data['kind']).to eq 'BUILD'
+ end
+ end
+
+ context 'when the job is a bridge' do
+ it 'returns BRIDGE' do
+ create(:ci_bridge, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ job_data = graphql_data_at(:project, :pipeline, :stages, :nodes, :groups, :nodes, :jobs, :nodes).first
+ expect(job_data['kind']).to eq 'BRIDGE'
+ end
+ end
+ end
+
describe '.jobs.artifacts' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 561d81f9860..6bacb3a59b2 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -11,16 +11,16 @@ RSpec.describe API::Members do
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
- project.add_developer(developer)
project.add_maintainer(maintainer)
+ project.add_developer(developer, current_user: maintainer)
project.request_access(access_requester)
end
end
let!(:group) do
create(:group, :public) do |group|
- group.add_developer(developer)
group.add_owner(maintainer)
+ group.add_developer(developer, maintainer)
create(:group_member, :minimal_access, source: group, user: user_with_minimal_access)
group.request_access(access_requester)
end
@@ -50,6 +50,10 @@ RSpec.describe API::Members do
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id]
+ expect(json_response).to contain_exactly(
+ a_hash_including('created_by' => a_hash_including('id' => maintainer.id)),
+ hash_not_including('created_by')
+ )
end
end
end
diff --git a/spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb b/spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb
new file mode 100644
index 00000000000..2fc286d607a
--- /dev/null
+++ b/spec/views/shared/_milestones_sort_dropdown.html.haml_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'shared/_milestones_sort_dropdown.html.haml' do
+ describe 'render' do
+ describe 'when a sort option is not selected' do
+ it 'renders a default sort option' do
+ render 'shared/milestones_sort_dropdown'
+
+ expect(rendered).to have_content 'Due soon'
+ end
+ end
+
+ describe 'when a sort option is selected' do
+ before do
+ assign(:sort, 'due_date_desc')
+
+ render 'shared/milestones_sort_dropdown'
+ end
+
+ it 'renders the selected sort option' do
+ expect(rendered).to have_content 'Due later'
+ end
+ end
+ end
+end