diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-23 03:12:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-23 03:12:54 +0300 |
commit | d70e88c64d472b3f82d5a36351b60313a9b4db11 (patch) | |
tree | 1b3616c8640adce4da13ae63b81e86a5c340ef72 | |
parent | ebaefcebccee0575e8dddde1fe17dabaae62459b (diff) |
Add latest changes from gitlab-org/gitlab@master
26 files changed, 403 insertions, 108 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index b9e18d3f15e..0b4f7ca1dd9 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -40,6 +40,9 @@ .if-automated-merge-request: &if-automated-merge-request if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "release-tools/update-gitaly" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /stable-ee$/' +.if-merge-request-targeting-stable-branch: &if-merge-request-targeting-stable-branch + if: '$CI_MERGE_REQUEST_IID && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^[\d-]+-stable(-ee)?$/' + .if-merge-request-labels-as-if-foss: &if-merge-request-labels-as-if-foss if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-as-if-foss/' @@ -576,6 +579,8 @@ when: never - <<: *if-security-merge-request when: never + - <<: *if-merge-request-targeting-stable-branch + when: never - <<: *if-merge-request-labels-as-if-jh - <<: *if-merge-request-labels-run-all-rspec - changes: *code-backstage-qa-patterns @@ -609,6 +614,8 @@ when: never - <<: *if-security-merge-request when: never + - <<: *if-merge-request-targeting-stable-branch + when: never - <<: *if-merge-request-labels-as-if-jh - <<: *if-merge-request-labels-run-all-rspec - <<: *if-merge-request @@ -1250,6 +1257,8 @@ when: never - <<: *if-security-merge-request when: never + - <<: *if-merge-request-targeting-stable-branch + when: never - <<: *if-merge-request-labels-as-if-jh allow_failure: true - <<: *if-merge-request @@ -1693,6 +1702,8 @@ when: never - <<: *if-security-merge-request when: never + - <<: *if-merge-request-targeting-stable-branch + when: never - <<: *if-merge-request-labels-as-if-jh - <<: *if-merge-request-labels-run-all-rspec - changes: *code-backstage-qa-patterns diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb index 75068014242..8a2693ee46b 100644 --- a/app/graphql/resolvers/project_jobs_resolver.rb +++ b/app/graphql/resolvers/project_jobs_resolver.rb @@ -33,6 +33,7 @@ module Resolvers def preloads { + previous_stage_jobs_and_needs: [:needs, :pipeline], artifacts: [:job_artifacts], pipeline: [:user] } diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index 5acd7f95606..ea733ab08ad 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -24,7 +24,6 @@ module Resolvers super end - # the preloads are defined on ee/app/graphql/ee/resolvers/project_pipeline_resolver.rb def resolve(iid: nil, sha: nil, **args) self.lookahead = args.delete(:lookahead) @@ -42,5 +41,11 @@ module Resolvers end end end + + def unconditional_includes + [ + { statuses: [:needs] } + ] + end end end diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb index 5a1e92efc96..23423b9274a 100644 --- a/app/graphql/resolvers/project_pipelines_resolver.rb +++ b/app/graphql/resolvers/project_pipelines_resolver.rb @@ -18,7 +18,7 @@ module Resolvers def preloads { - jobs: [:statuses], + jobs: { statuses: [:needs] }, upstream: [:triggered_by_pipeline], downstream: [:triggered_pipelines] } diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 48bd91bfc5b..57ff35bcaa5 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -50,6 +50,8 @@ module Types null: true, description: 'How long the job was enqueued before starting.' + field :previous_stage_jobs_and_needs, Types::Ci::JobType.connection_type, null: true, + description: 'All prerequisite jobs.' field :detailed_status, Types::Ci::DetailedStatusType, null: true, description: 'Detailed status of the job.' field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true, @@ -101,6 +103,26 @@ module Types end end + def previous_stage_jobs_and_needs + Gitlab::Graphql::Lazy.with_value(previous_stage_jobs) do |jobs| + (jobs + object.needs).uniq(&:name) + end + end + + def previous_stage_jobs + BatchLoader::GraphQL.for([object.pipeline, object.stage_idx - 1]).batch(default_value: []) do |tuples, loader| + tuples.group_by(&:first).each do |pipeline, keys| + positions = keys.map(&:second) + + stages = pipeline.stages.by_position(positions) + + stages.each do |stage| + loader.call([pipeline, stage.position], stage.latest_statuses) + end + end + end + end + def stage ::Gitlab::Graphql::Lazy.with_value(pipeline) do |pl| BatchLoader::GraphQL.for([pl, object.stage]).batch do |ids, loader| diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb index c0d931b3d31..fc887c83da3 100644 --- a/app/graphql/types/ci/stage_type.rb +++ b/app/graphql/types/ci/stage_type.rb @@ -31,7 +31,10 @@ module Types BatchLoader::GraphQL.for(key).batch(default_value: []) do |keys, loader| by_pipeline = keys.group_by(&:pipeline) - include_needs = keys.any? { |k| k.requires?(%i[nodes jobs nodes needs]) } + include_needs = keys.any? do |k| + k.requires?(%i[nodes jobs nodes needs]) || + k.requires?(%i[nodes jobs nodes previousStageJobsAndNeeds]) + end by_pipeline.each do |pl, key_group| project = pl.project diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index e2b15497638..8c4e97ac840 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -22,6 +22,7 @@ module Ci scope :ordered, -> { order(position: :asc) } scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) } scope :by_name, ->(names) { where(name: names) } + scope :by_position, ->(positions) { where(position: positions) } with_options unless: :importing? do validates :project, presence: true diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 1736fe82ca5..36b45dde6b4 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -64,6 +64,13 @@ module Namespaces traversal_ids.present? end + def use_traversal_ids_for_ancestors_upto? + return false unless use_traversal_ids? + return false unless Feature.enabled?(:use_traversal_ids_for_ancestors_upto, root_ancestor, default_enabled: :yaml) + + traversal_ids.present? + end + def use_traversal_ids_for_root_ancestor? return false unless Feature.enabled?(:use_traversal_ids_for_root_ancestor, default_enabled: :yaml) @@ -114,6 +121,35 @@ module Namespaces hierarchy_order == :desc ? traversal_ids[0..-2] : traversal_ids[0..-2].reverse end + # Returns all ancestors upto but excluding the top. + # When no top is given, all ancestors are returned. + # When top is not found, returns all ancestors. + # + # This copies the behavior of the recursive method. We will deprecate + # this behavior soon. + def ancestors_upto(top = nil, hierarchy_order: nil) + return super unless use_traversal_ids_for_ancestors_upto? + + # We can't use a default value in the method definition above because + # we need to preserve those specific parameters for super. + hierarchy_order ||= :desc + + # Get all ancestor IDs inclusively between top and our parent. + top_index = top ? traversal_ids.find_index(top.id) : 0 + ids = traversal_ids[top_index...-1] + ids_string = ids.map { |id| Integer(id) }.join(',') + + # WITH ORDINALITY lets us order the result to match traversal_ids order. + from_sql = <<~SQL + unnest(ARRAY[#{ids_string}]::bigint[]) WITH ORDINALITY AS ancestors(id, ord) + INNER JOIN namespaces ON namespaces.id = ancestors.id + SQL + + self.class + .from(Arel.sql(from_sql)) + .order('ancestors.ord': hierarchy_order) + end + def self_and_ancestors(hierarchy_order: nil) return super unless use_traversal_ids_for_ancestors? diff --git a/app/models/namespaces/traversal/recursive.rb b/app/models/namespaces/traversal/recursive.rb index 8d2c5d3be5a..53eac27aa54 100644 --- a/app/models/namespaces/traversal/recursive.rb +++ b/app/models/namespaces/traversal/recursive.rb @@ -46,6 +46,7 @@ module Namespaces object_hierarchy(self.class.where(id: id)) .ancestors(upto: top, hierarchy_order: hierarchy_order) end + alias_method :recursive_ancestors_upto, :ancestors_upto def self_and_ancestors(hierarchy_order: nil) return self.class.where(id: id) unless parent_id diff --git a/config/feature_flags/development/ci_build_tags_limit.yml b/config/feature_flags/development/use_traversal_ids_for_ancestors_upto.yml index 8aaa03a87e3..4fe7ca695c3 100644 --- a/config/feature_flags/development/ci_build_tags_limit.yml +++ b/config/feature_flags/development/use_traversal_ids_for_ancestors_upto.yml @@ -1,8 +1,8 @@ --- -name: ci_build_tags_limit -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68380 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338929 -milestone: '14.2' +name: use_traversal_ids_for_ancestors_upto +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72662 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/343619 +milestone: '14.6' type: development -group: group::pipeline execution -default_enabled: true +group: group::access +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b86ffbe0d84..0400dc31128 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -8663,6 +8663,7 @@ Represents the total number of issues and their weights for a particular day. | <a id="cijobneeds"></a>`needs` | [`CiBuildNeedConnection`](#cibuildneedconnection) | References to builds that must complete before the jobs run. (see [Connections](#connections)) | | <a id="cijobpipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. | | <a id="cijobplayable"></a>`playable` | [`Boolean!`](#boolean) | Indicates the job can be played. | +| <a id="cijobpreviousstagejobsandneeds"></a>`previousStageJobsAndNeeds` | [`CiJobConnection`](#cijobconnection) | All prerequisite jobs. (see [Connections](#connections)) | | <a id="cijobqueuedat"></a>`queuedAt` | [`Time`](#time) | When the job was enqueued and marked as pending. | | <a id="cijobqueuedduration"></a>`queuedDuration` | [`Duration`](#duration) | How long the job was enqueued before starting. | | <a id="cijobrefname"></a>`refName` | [`String`](#string) | Ref name of the job. | diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md index fbfcdcbf64f..1d6b76da5b9 100644 --- a/doc/ci/ci_cd_for_external_repos/index.md +++ b/doc/ci/ci_cd_for_external_repos/index.md @@ -9,33 +9,30 @@ type: index, howto >[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4642) in GitLab 10.6. -GitLab CI/CD can be used with: - -- [GitHub](github_integration.md). -- [Bitbucket Cloud](bitbucket_integration.md). -- Any other Git server. +GitLab CI/CD can be used with [GitHub](github_integration.md), [Bitbucket Cloud](bitbucket_integration.md), +or any other Git server. Instead of moving your entire project to GitLab, you can connect your external repository to get the benefits of GitLab CI/CD. Connecting an external repository sets up [repository mirroring](../../user/project/repository/mirror/index.md) -and create a lightweight project with issues, merge requests, wiki, and +and creates a lightweight project with issues, merge requests, wiki, and snippets disabled. These features [can be re-enabled later](../../user/project/settings/index.md#sharing-and-permissions). +## Connect to an external repository + To connect to an external repository: <!-- vale gitlab.Spelling = NO --> -1. On the top menu, select **Projects > Create new project**. +1. On the top bar, select **Menu > Projects > Create new project**. 1. Select **Run CI/CD for external repository**. 1. Select **GitHub** or **Repo by URL**. 1. Complete the fields. <!-- vale gitlab.Spelling = YES --> -![CI/CD for external repository project creation](img/ci_cd_for_external_repo.png) - ## Pipelines for external pull requests > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/65139) in GitLab 12.3. diff --git a/doc/development/documentation/site_architecture/deployment_process.md b/doc/development/documentation/site_architecture/deployment_process.md index c038ee96dbf..25bc699c9d4 100644 --- a/doc/development/documentation/site_architecture/deployment_process.md +++ b/doc/development/documentation/site_architecture/deployment_process.md @@ -50,8 +50,8 @@ that stable documentation and deploys it to the registry. For example: - [13.12 merge request pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/395365202). - [12.10 merge request pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/395365405). -In particular, the [`image:docs-single` job](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/4c18963fe0a414ad62f55b9e18f922588b2dd155/.gitlab-ci.yml#L655) in each pipeline -takes what is built, and pushes it to the [container registry](https://gitlab.com/gitlab-org/gitlab-docs/container_registry/631635). +In particular, the [`image:docs-single` job](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/4c18963fe0a414ad62f55b9e18f922588b2dd155/.gitlab-ci.yml#L655) in each pipeline runs automatically. +It takes what is built, and pushes it to the [container registry](https://gitlab.com/gitlab-org/gitlab-docs/container_registry/631635). ```mermaid graph TD @@ -91,6 +91,7 @@ The [`image:docs-latest` job](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/4 - Pulls the latest documentation from the default branches of the relevant upstream projects. - Pulls the Docker images previously built by the `image:docs-single` jobs. +- Must be run manually on a scheduled pipeline. For example, [a pipeline](https://gitlab.com/gitlab-org/gitlab-docs/-/pipelines/399233948) containing the [`image:docs-latest` job](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/1733948330): @@ -122,7 +123,8 @@ graph TD for it must be deployed to become available. The [`pages`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/4c18963fe0a414ad62f55b9e18f922588b2dd155/.gitlab-ci.yml#L491) -job runs the necessary commands to combine: +job runs automatically when a pipeline runs on the default branch (`main`). +It runs the necessary commands to combine: - A very up-to-date build of the `gitlab-docs` site code. - The latest docs from the default branches of the upstream projects. diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index adf0a115c6e..283576fb4e9 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -52,7 +52,8 @@ Prerequisite: To add groups to a project: -1. Go to your project and select **Project information > Members**. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Members**. 1. On the **Invite group** tab, under **Select a group to invite**, choose a group. 1. Select the highest max [role](../../permissions.md) for users in the group. 1. Optional. Choose an expiration date. On that date, the user can no longer access the project. @@ -75,7 +76,8 @@ Prerequisite: To import users: -1. Go to your project and select **Project information > Members**. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Members**. 1. On the **Invite member** tab, at the bottom of the panel, select **Import**. 1. Select the project. You can view only the projects for which you're a maintainer. 1. Select **Import project members**. @@ -115,7 +117,8 @@ Prerequisites: To remove a member from a project: -1. Go to your project and select **Project information > Members**. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Members**. 1. Next to the project member you want to remove, select **Remove member** **{remove}**. 1. Optional. In the confirmation box, select the **Also unassign this user from related issues and merge requests** checkbox. @@ -136,7 +139,8 @@ You can filter and sort members in a project. ### Display inherited members -1. Go to your project and select **Project information > Members**. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Members**. 1. In the **Filter members** box, select `Membership` `=` `Inherited`. 1. Press Enter. @@ -144,7 +148,8 @@ You can filter and sort members in a project. ### Display direct members -1. Go to your project and select **Project information > Members**. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Members**. 1. In the **Filter members** box, select `Membership` `=` `Direct`. 1. Press Enter. @@ -166,7 +171,7 @@ You can sort members by **Account**, **Access granted**, **Max role**, or **Last GitLab users can request to become a member of a project. -1. Go to the project you'd like to be a member of. +1. On the top bar, select **Menu > Projects** and find the project you want to be a member of. 1. By the project name, select **Request Access**. ![Request access button](img/request_access_button.png) @@ -189,8 +194,9 @@ Prerequisite: - You must be the project owner. -1. Go to the project and select **Settings > General**. -1. Expand the **Visibility, project features, permissions** section. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > General**. +1. Expand **Visibility, project features, permissions**. 1. Under **Project visibility**, select **Users can request access**. 1. Select **Save changes**. @@ -213,7 +219,8 @@ This feature might not be available to you. Check the **version history** note a In GitLab 13.11, you can optionally replace the form to add a member with a modal window. To add a member after enabling this feature: -1. Go to your project and select **Project information > Members**. +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Project information > Members**. 1. Select **Invite members**. 1. Enter an email address and select a role. 1. Optional. Select an **Access expiration date**. diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md index 8ca7f7defb2..cf35f082880 100644 --- a/doc/user/workspace/index.md +++ b/doc/user/workspace/index.md @@ -6,6 +6,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Workspace +DISCLAIMER: +This page contains information related to upcoming products, features, and functionality. +It is important to note that the information presented is for informational purposes only. +Please do not rely on this information for purchasing or planning purposes. +As with all projects, the items mentioned on this page are subject to change or delay. +The development, release, and timing of any products, features, or functionality remain at the +sole discretion of GitLab Inc. + Workspace will be above the [top-level namespaces](../group/index.md#namespaces) for you to manage everything you do as a GitLab administrator, including: diff --git a/lib/gitlab/ci/config/entry/tags.rb b/lib/gitlab/ci/config/entry/tags.rb index ca3b48372e2..6044cfddbdc 100644 --- a/lib/gitlab/ci/config/entry/tags.rb +++ b/lib/gitlab/ci/config/entry/tags.rb @@ -16,8 +16,6 @@ module Gitlab validates :config, array_of_strings: true validate do - next unless ::Feature.enabled?(:ci_build_tags_limit, default_enabled: :yaml) - if config.is_a?(Array) && config.size >= TAGS_LIMIT errors.add(:config, _("must be less than the limit of %{tag_limit} tags") % { tag_limit: TAGS_LIMIT }) end diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index e95a7da4fe5..0da2e291d3c 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -25,6 +25,7 @@ RSpec.describe Types::Ci::JobType do needs pipeline playable + previousStageJobsAndNeeds queued_at queued_duration refName diff --git a/spec/lib/gitlab/ci/config/entry/tags_spec.rb b/spec/lib/gitlab/ci/config/entry/tags_spec.rb index 79317de373b..e05d4ae52b2 100644 --- a/spec/lib/gitlab/ci/config/entry/tags_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/tags_spec.rb @@ -36,25 +36,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Tags do context 'when tags limit is reached' do let(:config) { Array.new(50) {|i| "tag-#{i}" } } - context 'when ci_build_tags_limit is enabled' do - before do - stub_feature_flags(ci_build_tags_limit: true) - end - - it 'reports error' do - expect(entry.errors) - .to include "tags config must be less than the limit of #{described_class::TAGS_LIMIT} tags" - end - end - - context 'when ci_build_tags_limit is disabled' do - before do - stub_feature_flags(ci_build_tags_limit: false) - end - - it 'does not report an error' do - expect(entry.errors).to be_empty - end + it 'reports error' do + expect(entry.errors) + .to include "tags config must be less than the limit of #{described_class::TAGS_LIMIT} tags" end end end diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 5e0fcb4882f..2b6f22e68f1 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -28,6 +28,18 @@ RSpec.describe Ci::Stage, :models do end end + describe '.by_position' do + it 'finds stages by position' do + a = create(:ci_stage_entity, position: 1) + b = create(:ci_stage_entity, position: 2) + c = create(:ci_stage_entity, position: 3) + + expect(described_class.by_position(1)).to contain_exactly(a) + expect(described_class.by_position(2)).to contain_exactly(b) + expect(described_class.by_position(%w[1 3])).to contain_exactly(a, c) + end + end + describe '.by_name' do it 'finds stages by name' do a = create(:ci_stage_entity, name: 'a') diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb index b29fa910ee6..d593d829dca 100644 --- a/spec/models/concerns/group_descendant_spec.rb +++ b/spec/models/concerns/group_descendant_spec.rb @@ -19,14 +19,16 @@ RSpec.describe GroupDescendant do query_count = ActiveRecord::QueryRecorder.new { test_group.hierarchy }.count - expect(query_count).to eq(1) + # use_traversal_ids_for_ancestors_upto actor based feature flag check adds an extra query. + expect(query_count).to eq(2) end it 'only queries once for the ancestors when a top is given' do test_group = create(:group, parent: subsub_group).reload recorder = ActiveRecord::QueryRecorder.new { test_group.hierarchy(subgroup) } - expect(recorder.count).to eq(1) + # use_traversal_ids_for_ancestors_upto actor based feature flag check adds an extra query. + expect(recorder.count).to eq(2) end it 'builds a hierarchy for a group' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 735aa4df2ba..6a2f9b3fbc0 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -533,6 +533,10 @@ RSpec.describe Group do describe '#ancestors' do it { expect(group.ancestors.to_sql).not_to include 'traversal_ids <@' } end + + describe '#ancestors_upto' do + it { expect(group.ancestors_upto.to_sql).not_to include "WITH ORDINALITY" } + end end context 'linear' do @@ -566,6 +570,10 @@ RSpec.describe Group do end end + describe '#ancestors_upto' do + it { expect(group.ancestors_upto.to_sql).to include "WITH ORDINALITY" } + end + context 'when project namespace exists in the group' do let!(:project) { create(:project, group: group) } let!(:project_namespace) { project.project_namespace } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 8f5860c799c..5adbcd0fa52 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -700,20 +700,6 @@ RSpec.describe Namespace do end end - describe '#ancestors_upto' do - let(:parent) { create(:group) } - let(:child) { create(:group, parent: parent) } - let(:child2) { create(:group, parent: child) } - - it 'returns all ancestors when no namespace is given' do - expect(child2.ancestors_upto).to contain_exactly(child, parent) - end - - it 'includes ancestors upto but excluding the given ancestor' do - expect(child2.ancestors_upto(parent)).to contain_exactly(child) - end - end - describe '#move_dir', :request_store do shared_examples "namespace restrictions" do context "when any project has container images" do @@ -1274,6 +1260,38 @@ RSpec.describe Namespace do end end + describe '#use_traversal_ids_for_ancestors_upto?' do + let_it_be(:namespace, reload: true) { create(:namespace) } + + subject { namespace.use_traversal_ids_for_ancestors_upto? } + + context 'when use_traversal_ids_for_ancestors_upto feature flag is true' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors_upto: true) + end + + it { is_expected.to eq true } + + it_behaves_like 'disabled feature flag when traversal_ids is blank' + end + + context 'when use_traversal_ids_for_ancestors_upto feature flag is false' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors_upto: false) + end + + it { is_expected.to eq false } + end + + context 'when use_traversal_ids? feature flag is false' do + before do + stub_feature_flags(use_traversal_ids: false) + end + + it { is_expected.to eq false } + end + end + describe '#users_with_descendants' do let(:user_a) { create(:user) } let(:user_b) { create(:user) } diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index e6362fdde88..f268036a78a 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'Query.project.pipeline' do describe '.stages.groups.jobs' do let(:pipeline) do pipeline = create(:ci_pipeline, project: project, user: user) - stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first') + stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first', position: 1) create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job') pipeline @@ -44,13 +44,18 @@ RSpec.describe 'Query.project.pipeline' do name jobs { nodes { - detailedStatus { - id - } name needs { nodes { #{all_graphql_fields_for('CiBuildNeed')} } } + previousStageJobsAndNeeds { + nodes { + name + } + } + detailedStatus { + id + } pipeline { id } @@ -62,58 +67,80 @@ RSpec.describe 'Query.project.pipeline' do FIELDS end - context 'when there are build needs' do - before do - pipeline.statuses.each do |build| - create_list(:ci_build_need, 2, build: build) - end - end - - it 'reports the build needs' do - post_graphql(query, current_user: user) - - expect(jobs_graphql_data).to contain_exactly a_hash_including( - 'needs' => a_hash_including( - 'nodes' => contain_exactly( - a_hash_including('name' => String), - a_hash_including('name' => String) - ) - ) - ) - end - end - it 'returns the jobs of a pipeline stage' do post_graphql(query, current_user: user) expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) end - describe 'performance' do + context 'when there is more than one stage and job needs' do before do build_stage = create(:ci_stage_entity, position: 2, name: 'build', project: project, pipeline: pipeline) test_stage = create(:ci_stage_entity, position: 3, name: 'test', project: project, pipeline: pipeline) - create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 1 2') - create(:commit_status, pipeline: pipeline, stage_id: build_stage.id, name: 'docker 2 2') - create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 1 2') - create(:commit_status, pipeline: pipeline, stage_id: test_stage.id, name: 'rspec 2 2') - end + deploy_stage = create(:ci_stage_entity, position: 4, name: 'deploy', project: project, pipeline: pipeline) - it 'can find the first stage' do - post_graphql(query, current_user: user, variables: first_n.with(1)) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_bridge, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build, pipeline: pipeline, stage_idx: deploy_stage.position, name: 'deploy 1 2', stage: deploy_stage) + deploy_job = create(:ci_build, pipeline: pipeline, stage_idx: deploy_stage.position, name: 'deploy 2 2', stage: deploy_stage) - expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job')) + create(:ci_build_need, build: test_job, name: 'my test job') + create(:ci_build_need, build: deploy_job, name: 'rspec 1 2') end - it 'can find all stages' do - post_graphql(query, current_user: user, variables: first_n.with(3)) + it 'reports the build needs and previous stages with no duplicates' do + post_graphql(query, current_user: user) expect(jobs_graphql_data).to contain_exactly( - a_hash_including('name' => 'my test job'), - a_hash_including('name' => 'docker 1 2'), - a_hash_including('name' => 'docker 2 2'), - a_hash_including('name' => 'rspec 1 2'), - a_hash_including('name' => 'rspec 2 2') + a_hash_including( + 'name' => 'my test job', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [] } + ), + a_hash_including( + 'name' => 'docker 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [ + { "name" => "my test job" } + ] } + ), + a_hash_including( + 'name' => 'docker 2 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [ + { "name" => "my test job" } + ] } + ), + a_hash_including( + 'name' => 'rspec 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [ + { "name" => "docker 1 2" }, { "name" => "docker 2 2" } + ] } + ), + a_hash_including( + 'name' => 'rspec 2 2', + 'needs' => { 'nodes' => [a_hash_including('name' => 'my test job')] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [ + { "name" => "docker 1 2" }, { "name" => "docker 2 2" }, { "name" => "my test job" } + ] } + ), + a_hash_including( + 'name' => 'deploy 1 2', + 'needs' => { 'nodes' => [] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [ + { "name" => "rspec 1 2" }, { "name" => "rspec 2 2" } + ] } + ), + a_hash_including( + 'name' => 'deploy 2 2', + 'needs' => { 'nodes' => [a_hash_including('name' => 'rspec 1 2')] }, + 'previousStageJobsAndNeeds' => { 'nodes' => [ + { "name" => "rspec 1 2" }, { "name" => "rspec 2 2" } + ] } + ) ) end diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb new file mode 100644 index 00000000000..1a823ede9ac --- /dev/null +++ b/spec/requests/api/graphql/project/jobs_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Query.project.jobs' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:user) { create(:user) } + + let(:pipeline) do + create(:ci_pipeline, project: project, user: user) + end + + let(:query) do + <<~QUERY + { + project(fullPath: "#{project.full_path}") { + jobs { + nodes { + name + previousStageJobsAndNeeds { + nodes { + name + } + } + } + } + } + } + QUERY + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build_need, build: test_job, name: 'docker 1 2') + + post_graphql(query, current_user: user) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: user) + end + + create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + + expect do + post_graphql(query, current_user: user) + end.not_to exceed_all_query_limit(control) + end +end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index d46ef313563..73e02e2a4b1 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -273,6 +273,48 @@ RSpec.describe 'getting pipeline information nested in a project' do end end + context 'N+1 queries on pipeline jobs' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + let(:fields) do + <<~FIELDS + jobs { + nodes { + previousStageJobsAndNeeds { + nodes { + name + } + } + } + } + FIELDS + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) + create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) + test_job = create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 2 2', stage: test_stage) + create(:ci_build_need, build: test_job, name: 'docker 1 2') + + post_graphql(query, current_user: current_user) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create(:ci_build, name: 'test-a', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + test_b_job = create(:ci_build, name: 'test-b', stage: test_stage, stage_idx: test_stage.position, pipeline: pipeline) + create(:ci_build_need, build: test_b_job, name: 'docker 2 2') + + expect do + post_graphql(query, current_user: current_user) + end.not_to exceed_all_query_limit(control) + end + end + context 'N+1 queries on stages jobs' do let(:depth) { 5 } let(:fields) do diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index ac6a843663f..73e22b97abc 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -205,6 +205,58 @@ RSpec.shared_examples 'namespace traversal' do end end + shared_examples '#ancestors_upto' do + let(:parent) { create(:group) } + let(:child) { create(:group, parent: parent) } + let(:child2) { create(:group, parent: child) } + + it 'returns all ancestors when no namespace is given' do + expect(child2.ancestors_upto).to contain_exactly(child, parent) + end + + it 'includes ancestors upto but excluding the given ancestor' do + expect(child2.ancestors_upto(parent)).to contain_exactly(child) + end + + context 'with asc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(child2.ancestors_upto(hierarchy_order: :asc)).to eq([child, parent]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(child2.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child]) + end + end + + describe '#recursive_self_and_ancestor_ids' do + it 'is equivalent to ancestors_upto' do + recursive_result = child2.recursive_ancestors_upto(parent) + linear_result = child2.ancestors_upto(parent) + expect(linear_result).to match_array recursive_result + end + + it 'makes a recursive query' do + expect { child2.recursive_ancestors_upto.try(:load) }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + + describe '#ancestors_upto' do + context 'with use_traversal_ids_for_ancestors_upto enabled' do + include_examples '#ancestors_upto' + end + + context 'with use_traversal_ids_for_ancestors_upto disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors_upto: false) + end + + include_examples '#ancestors_upto' + end + end + describe '#descendants' do let!(:another_group) { create(:group) } let!(:another_group_nested) { create(:group, parent: another_group) } |