summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-18 21:08:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-18 21:08:44 +0000
commitc75f38907c1b28adf5f57a8ad34df9f86c36d4e7 (patch)
tree2ddd657750116ea9460f76f439780af3ab255edb
parentbc578c5f89ff9d8ec03fbbd014714f9d1e5cb172 (diff)
downloadgitlab-ce-c75f38907c1b28adf5f57a8ad34df9f86c36d4e7.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/graphql/mutations/ci/job/cancel.rb28
-rw-r--r--app/graphql/mutations/ci/job/unschedule.rb28
-rw-r--r--app/graphql/types/mutation_type.rb2
-rw-r--r--app/models/application_record.rb16
-rw-r--r--app/models/integration.rb2
-rw-r--r--app/models/namespaces/traversal/linear.rb8
-rw-r--r--app/models/project.rb8
-rw-r--r--config/feature_flags/development/linear_project_ancestors.yml8
-rw-r--r--config/feature_flags/development/optimize_safe_find_or_create_by.yml8
-rw-r--r--doc/api/graphql/reference/index.md38
-rw-r--r--doc/api/packages/debian.md18
-rw-r--r--doc/api/packages/debian_project_distributions.md18
-rw-r--r--doc/development/sql.md23
-rw-r--r--doc/user/admin_area/license.md10
-rw-r--r--doc/user/project/deploy_tokens/index.md10
-rw-r--r--spec/models/application_record_spec.rb33
-rw-r--r--spec/models/gpg_signature_spec.rb6
-rw-r--r--spec/models/project_spec.rb106
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb48
20 files changed, 404 insertions, 59 deletions
diff --git a/app/graphql/mutations/ci/job/cancel.rb b/app/graphql/mutations/ci/job/cancel.rb
new file mode 100644
index 00000000000..dc9f4d19779
--- /dev/null
+++ b/app/graphql/mutations/ci/job/cancel.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Cancel < Base
+ graphql_name 'JobCancel'
+
+ field :job,
+ Types::Ci::JobType,
+ null: true,
+ description: 'Job after the mutation.'
+
+ authorize :update_build
+
+ def resolve(id:)
+ job = authorized_find!(id: id)
+
+ ::Ci::BuildCancelService.new(job, current_user).execute
+ {
+ job: job,
+ errors: errors_on_object(job)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/job/unschedule.rb b/app/graphql/mutations/ci/job/unschedule.rb
new file mode 100644
index 00000000000..07b1896bd2c
--- /dev/null
+++ b/app/graphql/mutations/ci/job/unschedule.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Job
+ class Unschedule < Base
+ graphql_name 'JobUnschedule'
+
+ field :job,
+ Types::Ci::JobType,
+ null: true,
+ description: 'Job after the mutation.'
+
+ authorize :update_build
+
+ def resolve(id:)
+ job = authorized_find!(id: id)
+
+ ::Ci::BuildUnscheduleService.new(job, current_user).execute
+ {
+ job: job,
+ errors: errors_on_object(job)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 530298ba07a..293d19d068a 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -99,6 +99,8 @@ module Types
mount_mutation Mutations::Ci::CiCdSettingsUpdate
mount_mutation Mutations::Ci::Job::Play
mount_mutation Mutations::Ci::Job::Retry
+ mount_mutation Mutations::Ci::Job::Cancel
+ mount_mutation Mutations::Ci::Job::Unschedule
mount_mutation Mutations::Ci::JobTokenScope::AddProject
mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
mount_mutation Mutations::Ci::Runner::Update, feature_flag: :runner_graphql_query
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index d362cfac4be..d9375b55e89 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -63,11 +63,27 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_find_or_create_by(*args, &block)
+ return optimized_safe_find_or_create_by(*args, &block) if Feature.enabled?(:optimize_safe_find_or_create_by, default_enabled: :yaml)
+
safe_ensure_unique(retries: 1) do
find_or_create_by(*args, &block)
end
end
+ def self.optimized_safe_find_or_create_by(*args, &block)
+ record = find_by(*args)
+ return record if record.present?
+
+ # We need to use `all.create` to make this implementation follow `find_or_create_by` which delegates this in
+ # https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/querying.rb#L22
+ #
+ # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
+ # and that skips some code that adds the newly created record to the association.
+ transaction(requires_new: true) { all.create(*args, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by(*args)
+ end
+
def create_or_load_association(association_name)
association(association_name).create unless association(association_name).loaded?
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 5c4d03f1fa8..a9c865569d0 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -274,7 +274,7 @@ class Integration < ApplicationRecord
end
def self.closest_group_integration(type, scope)
- group_ids = scope.ancestors.select(:id)
+ group_ids = scope.ancestors(hierarchy_order: :asc).select(:id)
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
where(type: type, group_id: group_ids, inherit_from_id: nil)
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 081e51c1028..33e8c3e5172 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -177,7 +177,13 @@ module Namespaces
if hierarchy_order
depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
- .order(depth: hierarchy_order)
+ # The SELECT includes an extra depth attribute. We wrap the SQL in a
+ # standard SELECT to avoid mismatched attribute errors when trying to
+ # chain future ActiveRelation commands, and retain the ordering.
+ skope = self.class
+ .without_sti_condition
+ .from(skope, self.class.table_name)
+ .order(depth: hierarchy_order)
end
skope
diff --git a/app/models/project.rb b/app/models/project.rb
index 38c0ebcc222..81b04e1316c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -914,7 +914,13 @@ class Project < ApplicationRecord
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
end
- alias_method :ancestors, :ancestors_upto
+ def ancestors(hierarchy_order: nil)
+ if Feature.enabled?(:linear_project_ancestors, self, default_enabled: :yaml)
+ group&.self_and_ancestors(hierarchy_order: hierarchy_order) || Group.none
+ else
+ ancestors_upto(hierarchy_order: hierarchy_order)
+ end
+ end
def ancestors_upto_ids(...)
ancestors_upto(...).pluck(:id)
diff --git a/config/feature_flags/development/linear_project_ancestors.yml b/config/feature_flags/development/linear_project_ancestors.yml
new file mode 100644
index 00000000000..28c8fbcbf59
--- /dev/null
+++ b/config/feature_flags/development/linear_project_ancestors.yml
@@ -0,0 +1,8 @@
+---
+name: linear_project_ancestors
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68072
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338403
+milestone: '14.2'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/optimize_safe_find_or_create_by.yml b/config/feature_flags/development/optimize_safe_find_or_create_by.yml
new file mode 100644
index 00000000000..6adedb5cece
--- /dev/null
+++ b/config/feature_flags/development/optimize_safe_find_or_create_by.yml
@@ -0,0 +1,8 @@
+---
+name: optimize_safe_find_or_create_by
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68458
+rollout_issue_url:
+milestone: '14.3'
+type: development
+group: group::database
+default_enabled: false
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 63781f9f353..54709f56eb8 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2882,6 +2882,25 @@ Input type: `JiraImportUsersInput`
| <a id="mutationjiraimportuserserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationjiraimportusersjirausers"></a>`jiraUsers` | [`[JiraUser!]`](#jirauser) | Users returned from Jira, matched by email and name if possible. |
+### `Mutation.jobCancel`
+
+Input type: `JobCancelInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationjobcancelclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationjobcancelid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationjobcancelclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationjobcancelerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationjobcanceljob"></a>`job` | [`CiJob`](#cijob) | Job after the mutation. |
+
### `Mutation.jobPlay`
Input type: `JobPlayInput`
@@ -2920,6 +2939,25 @@ Input type: `JobRetryInput`
| <a id="mutationjobretryerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationjobretryjob"></a>`job` | [`CiJob`](#cijob) | Job after the mutation. |
+### `Mutation.jobUnschedule`
+
+Input type: `JobUnscheduleInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationjobunscheduleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationjobunscheduleid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationjobunscheduleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationjobunscheduleerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationjobunschedulejob"></a>`job` | [`CiJob`](#cijob) | Job after the mutation. |
+
### `Mutation.labelCreate`
Input type: `LabelCreateInput`
diff --git a/doc/api/packages/debian.md b/doc/api/packages/debian.md
index 0912f894fa3..154c99d7e0a 100644
--- a/doc/api/packages/debian.md
+++ b/doc/api/packages/debian.md
@@ -28,22 +28,10 @@ for details on which headers and token types are supported.
## Enable the Debian API
-Debian repository support is still a work in progress. It's gated behind a feature flag that's
-**disabled by default**.
+The Debian API is behind a feature flag that is disabled by default.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can opt to enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:debian_packages)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:debian_packages)
-```
+can opt to enable it. To enable it, follow the instructions in
+[Enable the Debian API](../../user/packages/debian_repository/index.md#enable-the-debian-api).
## Enable the Debian group API
diff --git a/doc/api/packages/debian_project_distributions.md b/doc/api/packages/debian_project_distributions.md
index 16079d1c5ab..bedf3f1f27a 100644
--- a/doc/api/packages/debian_project_distributions.md
+++ b/doc/api/packages/debian_project_distributions.md
@@ -20,22 +20,10 @@ For more information about working with Debian packages, see the
## Enable the Debian API
-Debian repository support is still a work in progress. It's gated behind a feature flag that's
-**disabled by default**.
+The Debian API is behind a feature flag that is disabled by default.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can opt to enable it.
-
-To enable it:
-
-```ruby
-Feature.enable(:debian_packages)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:debian_packages)
-```
+can opt to enable it. To enable it, follow the instructions in
+[Enable the Debian API](../../user/packages/debian_repository/index.md#enable-the-debian-api).
## List all Debian distributions in a project
diff --git a/doc/development/sql.md b/doc/development/sql.md
index 4f181d01505..40ee19c0b9e 100644
--- a/doc/development/sql.md
+++ b/doc/development/sql.md
@@ -429,14 +429,33 @@ Using transactions does not solve this problem.
To solve this we've added the `ApplicationRecord.safe_find_or_create_by`.
-This method can be used just as you would the normal
-`find_or_create_by` but it wraps the call in a *new* transaction and
+This method can be used the same way as
+`find_or_create_by`, but it wraps the call in a *new* transaction (or a subtransaction) and
retries if it were to fail because of an
`ActiveRecord::RecordNotUnique` error.
To be able to use this method, make sure the model you want to use
this on inherits from `ApplicationRecord`.
+In Rails 6 and later, there is a
+[`.create_or_find_by`](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-create_or_find_by)
+method. This method differs from our `.safe_find_or_create_by` methods
+because it performs the `INSERT`, and then performs the `SELECT` commands only if that call
+fails.
+
+If the `INSERT` fails, it will leave a dead tuple around and
+increment the primary key sequence (if any), among [other downsides](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-create_or_find_by).
+
+We prefer `.safe_find_or_create_by` if the common path is that we
+have a single record which is reused after it has first been created.
+However, if the more common path is to create a new record, and we only
+want to avoid duplicate records to be inserted on edge cases
+(for example a job-retry), then `.create_or_find_by` can save us a `SELECT`.
+
+Both methods use subtransactions internally if executed within the context of
+an existing transaction. This can significantly impact overall performance,
+especially if more than 64 live subtransactions are being used inside a single transaction.
+
## Monitor SQL queries in production
GitLab team members can monitor slow or canceled queries on GitLab.com
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index 49ffc82982a..0431b0d1628 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -153,6 +153,16 @@ additional users. More information on how to determine the required number of us
and how to add additional seats can be found in the
[licensing FAQ](https://about.gitlab.com/pricing/licensing-faq/).
+In GitLab 14.2 and later, for instances that use a license file, you can exceed the number of purchased users and still activate your license.
+
+- If the users over license are less than or equal to 10% of the users in the subscription,
+ the license is applied and the overage is paid in the next true-up.
+- If the users over license are more than 10% of the users in the subscription,
+ you cannot apply the license without purchasing more users.
+
+For example, if you purchased a license for 100 users, you can have 110 users when you activate
+your license. However, if you have 111, you must purchase more users before you can activate.
+
### There is a connectivity issue
In GitLab 14.1 and later, to activate your subscription, your GitLab instance must be connected to the internet.
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 2f4eb75fe4c..70363b67c88 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -173,6 +173,16 @@ To use a group deploy token:
The scopes applied to a group deploy token (such as `read_repository`)
apply consistently when cloning the repository of related projects.
+### Pull images from the Dependency Proxy
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280586) in GitLab 14.2.
+
+To pull images from the Dependency Proxy, you must:
+
+1. Create a group deploy token with both `read_registry` and `write_registry` scopes.
+1. Take note of your `username` and `token`.
+1. Follow the Depenency Proxy [authentication instructions](../../packages/dependency_proxy/index.md).
+
### GitLab deploy token
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18414) in GitLab 10.8.
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index dbef71431a2..f9a05c720a3 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -39,13 +39,14 @@ RSpec.describe ApplicationRecord do
let(:suggestion_attributes) { attributes_for(:suggestion).merge!(note_id: note.id) }
- describe '.safe_find_or_create_by' do
+ shared_examples '.safe_find_or_create_by' do
it 'creates the suggestion avoiding race conditions' do
- expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
- allow(Suggestion).to receive(:find_or_create_by).and_call_original
+ existing_suggestion = double(:Suggestion)
- expect { Suggestion.safe_find_or_create_by(suggestion_attributes) }
- .to change { Suggestion.count }.by(1)
+ expect(Suggestion).to receive(:find_by).and_return(nil, existing_suggestion)
+ expect(Suggestion).to receive(:create).and_raise(ActiveRecord::RecordNotUnique)
+
+ expect(Suggestion.safe_find_or_create_by(suggestion_attributes)).to eq(existing_suggestion)
end
it 'passes a block to find_or_create_by' do
@@ -62,10 +63,8 @@ RSpec.describe ApplicationRecord do
end
end
- describe '.safe_find_or_create_by!' do
+ shared_examples '.safe_find_or_create_by!' do
it 'creates a record using safe_find_or_create_by' do
- expect(Suggestion).to receive(:find_or_create_by).and_call_original
-
expect(Suggestion.safe_find_or_create_by!(suggestion_attributes))
.to be_a(Suggestion)
end
@@ -89,6 +88,24 @@ RSpec.describe ApplicationRecord do
.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ context 'when optimized_safe_find_or_create_by is enabled' do
+ before do
+ stub_feature_flags(optimized_safe_find_or_create_by: true)
+ end
+
+ it_behaves_like '.safe_find_or_create_by'
+ it_behaves_like '.safe_find_or_create_by!'
+ end
+
+ context 'when optimized_safe_find_or_create_by is disabled' do
+ before do
+ stub_feature_flags(optimized_safe_find_or_create_by: false)
+ end
+
+ it_behaves_like '.safe_find_or_create_by'
+ it_behaves_like '.safe_find_or_create_by!'
+ end
end
describe '.underscore' do
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index 997d9bbec72..7a1799c670e 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -54,8 +54,10 @@ RSpec.describe GpgSignature do
end
it 'does not raise an error in case of a race condition' do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
- allow(described_class).to receive(:find_or_create_by).and_call_original
+ expect(described_class).to receive(:find_by).and_return(nil, double(described_class, persisted?: true))
+
+ expect(described_class).to receive(:create).and_raise(ActiveRecord::RecordNotUnique)
+ allow(described_class).to receive(:create).and_call_original
described_class.safe_create!(attributes)
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2945a842ebc..d8f3a63d221 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Project, factory_default: :keep do
include ProjectForksHelper
include GitHelpers
include ExternalAuthorizationServiceHelpers
+ include ReloadHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:namespace) { create_default(:namespace).freeze }
@@ -3021,29 +3022,106 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#ancestors_upto' do
- let_it_be(:parent) { create(:group) }
- let_it_be(:child) { create(:group, parent: parent) }
- let_it_be(:child2) { create(:group, parent: child) }
- let_it_be(:project) { create(:project, namespace: child2) }
+ shared_context 'project with group ancestry' do
+ let(:parent) { create(:group) }
+ let(:child) { create(:group, parent: parent) }
+ let(:child2) { create(:group, parent: child) }
+ let(:project) { create(:project, namespace: child2) }
+
+ before do
+ reload_models(parent, child, child2)
+ end
+ end
+
+ shared_context 'project with namespace ancestry' do
+ let(:namespace) { create :namespace }
+ let(:project) { create :project, namespace: namespace }
+ end
- it 'returns all ancestors when no namespace is given' do
- expect(project.ancestors_upto).to contain_exactly(child2, child, parent)
+ shared_examples 'project with group ancestors' do
+ it 'returns all ancestors' do
+ is_expected.to contain_exactly(child2, child, parent)
end
+ end
+
+ shared_examples 'project with ordered group ancestors' do
+ let(:hierarchy_order) { :desc }
- it 'includes ancestors upto but excluding the given ancestor' do
- expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
+ it 'returns ancestors ordered by descending hierarchy' do
+ is_expected.to eq([parent, child, child2])
end
+ end
+
+ shared_examples '#ancestors' do
+ context 'group ancestory' do
+ include_context 'project with group ancestry'
- describe 'with hierarchy_order' do
- it 'returns ancestors ordered by descending hierarchy' do
- expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
+ it_behaves_like 'project with group ancestors' do
+ subject { project.ancestors }
end
- it 'can be used with upto option' do
- expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
+ it_behaves_like 'project with ordered group ancestors' do
+ subject { project.ancestors(hierarchy_order: hierarchy_order) }
end
end
+
+ context 'namespace ancestry' do
+ include_context 'project with namespace ancestry'
+
+ subject { project.ancestors }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '#ancestors' do
+ context 'with linear_project_ancestors feature flag enabled' do
+ before do
+ stub_feature_flags(linear_project_ancestors: true)
+ end
+
+ include_examples '#ancestors'
+ end
+
+ context 'with linear_project_ancestors feature flag disabled' do
+ before do
+ stub_feature_flags(linear_project_ancestors: false)
+ end
+
+ include_examples '#ancestors'
+ end
+ end
+
+ describe '#ancestors_upto' do
+ context 'group ancestry' do
+ include_context 'project with group ancestry'
+
+ it_behaves_like 'project with group ancestors' do
+ subject { project.ancestors_upto }
+ end
+
+ it_behaves_like 'project with ordered group ancestors' do
+ subject { project.ancestors_upto(hierarchy_order: hierarchy_order) }
+ end
+
+ it 'includes ancestors upto but excluding the given ancestor' do
+ expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
+ end
+
+ describe 'with hierarchy_order' do
+ it 'can be used with upto option' do
+ expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
+ end
+ end
+ end
+
+ context 'namespace ancestry' do
+ include_context 'project with namespace ancestry'
+
+ subject { project.ancestors_upto }
+
+ it { is_expected.to be_empty }
+ end
end
describe '#root_ancestor' do
diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
new file mode 100644
index 00000000000..ee0f0a9bccb
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "JobCancel" do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:job) { create(:ci_build, pipeline: pipeline, name: 'build') }
+
+ let(:mutation) do
+ variables = {
+ id: job.to_global_id.to_s
+ }
+ graphql_mutation(:job_cancel, variables,
+ <<-QL
+ errors
+ job {
+ id
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:job_cancel) }
+
+ it 'returns an error if the user is not allowed to cancel the job' do
+ project.add_developer(user)
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'cancels a job' do
+ job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s
+ project.add_maintainer(user)
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['job']['id']).to eq(job_id)
+ expect(job.reload.status).to eq('canceled')
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
new file mode 100644
index 00000000000..4ddc019a2b5
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'JobUnschedule' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:job) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'build') }
+
+ let(:mutation) do
+ variables = {
+ id: job.to_global_id.to_s
+ }
+ graphql_mutation(:job_unschedule, variables,
+ <<-QL
+ errors
+ job {
+ id
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:job_unschedule) }
+
+ it 'returns an error if the user is not allowed to unschedule the job' do
+ project.add_developer(user)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ expect(job.reload.status).to eq('scheduled')
+ end
+
+ it 'unschedules a job' do
+ project.add_maintainer(user)
+
+ job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['job']['id']).to eq(job_id)
+ expect(job.reload.status).to eq('manual')
+ end
+end