summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITLAB_ELASTICSEARCH_INDEXER_VERSION2
-rw-r--r--app/models/ci/pipeline_schedule.rb2
-rw-r--r--app/workers/pipeline_schedule_worker.rb6
-rw-r--r--db/migrate/20200207132752_add_es_bulk_config.rb11
-rw-r--r--db/schema.rb2
-rw-r--r--doc/integration/elasticsearch.md2
-rw-r--r--doc/user/project/integrations/img/prometheus_dashboard_stacked_column_panel_type_v12_8.pngbin0 -> 13898 bytes
-rw-r--r--doc/user/project/integrations/prometheus.md38
-rw-r--r--doc/user/project/merge_requests/img/scoped_to_protected_branch_v12_8.pngbin0 -> 91714 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md16
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/frontend/environments/environments_app_spec.js168
-rw-r--r--spec/javascripts/environments/environments_app_spec.js279
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb4
14 files changed, 371 insertions, 174 deletions
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
index 227cea21564..7ec1d6db408 100644
--- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -1 +1 @@
-2.0.0
+2.1.0
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 9a1445e624c..f5785000062 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -23,7 +23,7 @@ module Ci
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
- scope :preloaded, -> { preload(:owner, :project) }
+ scope :preloaded, -> { preload(:owner, project: [:route]) }
accepts_nested_attributes_for :variables, allow_destroy: true
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 841308611eb..8b326b9dbb6 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -2,7 +2,7 @@
class PipelineScheduleWorker
include ApplicationWorker
- include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+ include CronjobQueue
feature_category :continuous_integration
worker_resource_boundary :cpu
@@ -10,7 +10,9 @@ class PipelineScheduleWorker
def perform
Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
schedules.each do |schedule|
- Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
+ with_context(project: schedule.project, user: schedule.owner) do
+ Ci::PipelineScheduleService.new(schedule.project, schedule.owner).execute(schedule)
+ end
end
end
end
diff --git a/db/migrate/20200207132752_add_es_bulk_config.rb b/db/migrate/20200207132752_add_es_bulk_config.rb
new file mode 100644
index 00000000000..c460971139c
--- /dev/null
+++ b/db/migrate/20200207132752_add_es_bulk_config.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddEsBulkConfig < ActiveRecord::Migration[6.0]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :elasticsearch_max_bulk_size_mb, :smallint, null: false, default: 10
+ add_column :application_settings, :elasticsearch_max_bulk_concurrency, :smallint, null: false, default: 10
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 162964f25e8..a6c2edabeaf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -344,6 +344,8 @@ ActiveRecord::Schema.define(version: 2020_02_11_152410) do
t.boolean "updating_name_disabled_for_users", default: false, null: false
t.integer "instance_administrators_group_id"
t.integer "elasticsearch_indexed_field_length_limit", default: 0, null: false
+ t.integer "elasticsearch_max_bulk_size_mb", limit: 2, default: 10, null: false
+ t.integer "elasticsearch_max_bulk_concurrency", limit: 2, default: 10, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index 5e0e1919ab7..3ef54ca6dd3 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -151,6 +151,8 @@ The following Elasticsearch settings are available:
| `AWS Access Key` | The AWS access key. |
| `AWS Secret Access Key` | The AWS secret access key. |
| `Maximum field length` | See [the explanation in instance limits.](../administration/instance_limits.md#maximum-field-length). |
+| `Maximum bulk request size (MiB)` | Repository indexing uses the Elasticsearch bulk request API. This setting determines the maximum size of an individual bulk request during these operations. |
+| `Bulk request concurrency` | Each repository indexing operation may submit bulk requests in parallel. This increases indexing performance, but fills the Elasticsearch bulk requests queue faster. |
### Limiting namespaces and projects
diff --git a/doc/user/project/integrations/img/prometheus_dashboard_stacked_column_panel_type_v12_8.png b/doc/user/project/integrations/img/prometheus_dashboard_stacked_column_panel_type_v12_8.png
new file mode 100644
index 00000000000..ba67509bcf3
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_dashboard_stacked_column_panel_type_v12_8.png
Binary files differ
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index bba281ef2ef..66c128314bb 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -356,6 +356,44 @@ Note the following properties:
![anomaly panel type](img/prometheus_dashboard_column_panel_type.png)
+##### Stacked column
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30583) in GitLab 12.8.
+
+To add a stacked column panel type to a dashboard, look at the following sample dashboard file:
+
+```yaml
+dashboard: 'Dashboard title'
+priority: 1
+panel_groups:
+- group: 'Group Title'
+ priority: 5
+ panels:
+ - type: 'stacked-column'
+ title: "Stacked column"
+ y_label: "y label"
+ x_label: 'x label'
+ metrics:
+ - id: memory_1
+ query_range: 'memory_query'
+ label: "memory query 1"
+ unit: "count"
+ series_name: 'group 1'
+ - id: memory_2
+ query_range: 'memory_query_2'
+ label: "memory query 2"
+ unit: "count"
+ series_name: 'group 2'
+
+```
+
+![stacked column panel type](img/prometheus_dashboard_stacked_column_panel_type_v12_8.png)
+
+| Property | Type | Required | Description |
+| ------ | ------ | ------ | ------ |
+| `type` | string | yes | Type of panel to be rendered. For stacked column panel types, set to `stacked-column` |
+| `query_range` | yes | yes | For stacked column panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) |
+
##### Single Stat
To add a single stat panel type to a dashboard, look at the following sample dashboard file:
diff --git a/doc/user/project/merge_requests/img/scoped_to_protected_branch_v12_8.png b/doc/user/project/merge_requests/img/scoped_to_protected_branch_v12_8.png
new file mode 100644
index 00000000000..08a24e9f28e
--- /dev/null
+++ b/doc/user/project/merge_requests/img/scoped_to_protected_branch_v12_8.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 854e3a87d65..d378c119aa8 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -147,6 +147,22 @@ reduce the number of approvals left for all rules that the approver belongs to.
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v12_7.png)
+### Scoped to Protected Branch **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/460) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
+
+Approval rules are often only relevant to specific branches, like `master`.
+When configuring [**Default Approval Rules**](#adding--editing-a-default-approval-rule)
+these can be scoped to all the protected branches at once by navigating to your project's
+**Settings**, expanding **Merge request approvals**, and selecting **Any branch** from
+the **Target branch** dropdown.
+
+Alternatively, you can select a very specific protected branch from the **Target branch** dropdown:
+
+![Scoped to Protected Branch](img/scoped_to_protected_branch_v12_8.png)
+
+To enable this configuration, see [Code Owner’s approvals for protected branches](../protected_branches.md#protected-branches-approval-by-code-owners-premium).
+
## Adding or removing an approval
When an [eligible approver](#eligible-approvers) visits an open merge request,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8298d575827..fceb327aed1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2999,6 +2999,9 @@ msgstr ""
msgid "Built-in"
msgstr ""
+msgid "Bulk request concurrency"
+msgstr ""
+
msgid "Burndown chart"
msgstr ""
@@ -11728,9 +11731,15 @@ msgstr ""
msgid "Maximum attachment size (MB)"
msgstr ""
+msgid "Maximum bulk request size (MiB)"
+msgstr ""
+
msgid "Maximum capacity"
msgstr ""
+msgid "Maximum concurrency of Elasticsearch bulk requests per indexing operation."
+msgstr ""
+
msgid "Maximum delay (Minutes)"
msgstr ""
@@ -11773,6 +11782,9 @@ msgstr ""
msgid "Maximum size limit for each repository."
msgstr ""
+msgid "Maximum size of Elasticsearch bulk indexing requests."
+msgstr ""
+
msgid "Maximum size of individual attachments in comments."
msgstr ""
@@ -19578,6 +19590,9 @@ msgstr ""
msgid "This namespace has already been taken! Please choose another one."
msgstr ""
+msgid "This only applies to repository indexing operations."
+msgstr ""
+
msgid "This option is only available on GitLab.com"
msgstr ""
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
deleted file mode 100644
index f3d2bd2462e..00000000000
--- a/spec/frontend/environments/environments_app_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import { mount, shallowMount } from '@vue/test-utils';
-import axios from '~/lib/utils/axios_utils';
-import MockAdapter from 'axios-mock-adapter';
-import Container from '~/environments/components/container.vue';
-import EmptyState from '~/environments/components/empty_state.vue';
-import EnvironmentsApp from '~/environments/components/environments_app.vue';
-import { environment, folder } from './mock_data';
-
-describe('Environment', () => {
- let mock;
- let wrapper;
-
- const mockData = {
- endpoint: 'environments.json',
- canCreateEnvironment: true,
- canReadEnvironment: true,
- newEnvironmentPath: 'environments/new',
- helpPagePath: 'help',
- canaryDeploymentFeatureId: 'canary_deployment',
- showCanaryDeploymentCallout: true,
- userCalloutsPath: '/callouts',
- lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
- helpCanaryDeploymentsPath: 'help/canary-deployments',
- };
-
- const mockRequest = (response, body) => {
- mock.onGet(mockData.endpoint).reply(response, body, {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- });
- };
-
- const createWrapper = (shallow = false) => {
- const fn = shallow ? shallowMount : mount;
- wrapper = fn(EnvironmentsApp, { propsData: mockData });
- return axios.waitForAll();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
- describe('successful request', () => {
- describe('without environments', () => {
- beforeEach(() => {
- mockRequest(200, { environments: [] });
- return createWrapper(true);
- });
-
- it('should render the empty state', () => {
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- });
-
- describe('when it is possible to enable a review app', () => {
- beforeEach(() => {
- mockRequest(200, { environments: [], review_app: { can_setup_review_app: true } });
- return createWrapper();
- });
-
- it('should render the enable review app button', () => {
- expect(wrapper.find('.js-enable-review-app-button').text()).toContain(
- 'Enable review app',
- );
- });
- });
- });
-
- describe('with paginated environments', () => {
- const environmentList = [environment];
-
- beforeEach(() => {
- mockRequest(200, {
- environments: environmentList,
- stopped_count: 1,
- available_count: 0,
- });
- return createWrapper();
- });
-
- it('should render a conatiner table with environments', () => {
- const containerTable = wrapper.find(Container);
-
- expect(containerTable.exists()).toBe(true);
- expect(containerTable.props('environments').length).toEqual(environmentList.length);
- expect(containerTable.find('.environment-name').text()).toEqual(environmentList[0].name);
- });
-
- describe('pagination', () => {
- it('should render pagination', () => {
- expect(wrapper.findAll('.gl-pagination li').length).toEqual(9);
- });
-
- it('should make an API request when page is clicked', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
-
- wrapper.find('.gl-pagination li:nth-child(3) .page-link').trigger('click');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
- });
-
- it('should make an API request when using tabs', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.find('.js-environments-tab-stopped').trigger('click');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
- });
- });
- });
- });
-
- describe('unsuccessful request', () => {
- beforeEach(() => {
- mockRequest(500, {});
- return createWrapper(true);
- });
-
- it('should render empty state', () => {
- expect(wrapper.find(EmptyState).exists()).toBe(true);
- });
- });
-
- describe('expandable folders', () => {
- beforeEach(() => {
- mockRequest(200, {
- environments: [folder],
- stopped_count: 1,
- available_count: 0,
- });
-
- mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
-
- return createWrapper().then(() => {
- // open folder
- wrapper.find('.folder-name').trigger('click');
- return axios.waitForAll();
- });
- });
-
- it('should open a closed folder', () => {
- expect(wrapper.find('.folder-icon.ic-chevron-right').exists()).toBe(false);
- });
-
- it('should close an opened folder', () => {
- expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(true);
-
- // close folder
- wrapper.find('.folder-name').trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.folder-icon.ic-chevron-down').exists()).toBe(false);
- });
- });
-
- it('should show children environments', () => {
- expect(wrapper.findAll('.js-child-row').length).toEqual(1);
- });
-
- it('should show a button to show all environments', () => {
- expect(wrapper.find('.text-center > a.btn').text()).toContain('Show all');
- });
- });
-});
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
new file mode 100644
index 00000000000..6c05b609923
--- /dev/null
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -0,0 +1,279 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import axios from '~/lib/utils/axios_utils';
+import environmentsComponent from '~/environments/components/environments_app.vue';
+import { environment, folder } from './mock_data';
+
+describe('Environment', () => {
+ const mockData = {
+ endpoint: 'environments.json',
+ canCreateEnvironment: true,
+ canReadEnvironment: true,
+ newEnvironmentPath: 'environments/new',
+ helpPagePath: 'help',
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
+ };
+
+ let EnvironmentsComponent;
+ let component;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ EnvironmentsComponent = Vue.extend(environmentsComponent);
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ mock.restore();
+ });
+
+ describe('successful request', () => {
+ describe('without environments', () => {
+ beforeEach(done => {
+ mock.onGet(mockData.endpoint).reply(200, { environments: [] });
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('should render the empty state', () => {
+ expect(component.$el.querySelector('.js-new-environment-button').textContent).toContain(
+ 'New environment',
+ );
+
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
+ "You don't have any environments right now",
+ );
+ });
+
+ describe('when it is possible to enable a review app', () => {
+ beforeEach(done => {
+ mock
+ .onGet(mockData.endpoint)
+ .reply(200, { environments: [], review_app: { can_setup_review_app: true } });
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('should render the enable review app button', () => {
+ expect(component.$el.querySelector('.js-enable-review-app-button').textContent).toContain(
+ 'Enable review app',
+ );
+ });
+ });
+ });
+
+ describe('with paginated environments', () => {
+ beforeEach(done => {
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: [environment],
+ stopped_count: 1,
+ available_count: 0,
+ },
+ {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ );
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('should render a table with environments', () => {
+ expect(component.$el.querySelectorAll('table')).not.toBeNull();
+ expect(component.$el.querySelector('.environment-name').textContent.trim()).toEqual(
+ environment.name,
+ );
+ });
+
+ describe('pagination', () => {
+ it('should render pagination', () => {
+ expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(9);
+ });
+
+ it('should make an API request when page is clicked', done => {
+ spyOn(component, 'updateContent');
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(3) .page-link').click();
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
+ done();
+ }, 0);
+ });
+
+ it('should make an API request when using tabs', done => {
+ setTimeout(() => {
+ spyOn(component, 'updateContent');
+ component.$el.querySelector('.js-environments-tab-stopped').click();
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
+ done();
+ }, 0);
+ });
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ beforeEach(done => {
+ mock.onGet(mockData.endpoint).reply(500, {});
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('should render empty state', () => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain(
+ "You don't have any environments right now",
+ );
+ });
+ });
+
+ describe('expandable folders', () => {
+ beforeEach(() => {
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: [folder],
+ stopped_count: 0,
+ available_count: 1,
+ },
+ {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ );
+
+ mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+ });
+
+ it('should open a closed folder', done => {
+ setTimeout(() => {
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.folder-icon.ic-chevron-right')).toBe(null);
+ done();
+ });
+ }, 0);
+ });
+
+ it('should close an opened folder', done => {
+ setTimeout(() => {
+ // open folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ // close folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.folder-icon.ic-chevron-down')).toBe(null);
+ done();
+ });
+ });
+ }, 0);
+ });
+
+ it('should show children environments and a button to show all environments', done => {
+ setTimeout(() => {
+ // open folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ // wait for next async request
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
+ expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain(
+ 'Show all',
+ );
+ done();
+ });
+ });
+ }, 0);
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ mock.onGet(mockData.endpoint).reply(
+ 200,
+ {
+ environments: [],
+ stopped_count: 0,
+ available_count: 1,
+ },
+ {},
+ );
+
+ component = mountComponent(EnvironmentsComponent, mockData);
+ spyOn(window.history, 'pushState').and.stub();
+ });
+
+ describe('updateContent', () => {
+ it('should set given parameters', done => {
+ component
+ .updateContent({ scope: 'stopped', page: '3' })
+ .then(() => {
+ expect(component.page).toEqual('3');
+ expect(component.scope).toEqual('stopped');
+ expect(component.requestData.scope).toEqual('stopped');
+ expect(component.requestData.page).toEqual('3');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ spyOn(component, 'updateContent');
+ component.onChangeTab('stopped');
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ spyOn(component, 'updateContent');
+ component.onChangePage(4);
+
+ expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ });
+ });
+ });
+});
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index aee43025288..4ed4b7e38d8 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -80,9 +80,9 @@ describe Ci::PipelineSchedule do
it 'preloads the associations' do
subject
- query = ActiveRecord::QueryRecorder.new { subject.each(&:project) }
+ query = ActiveRecord::QueryRecorder.new { subject.map(&:project).each(&:route) }
- expect(query.count).to eq(2)
+ expect(query.count).to eq(3)
end
end