Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-23 21:10:54 +00:00
parent 6a6e9bec88
commit e79ee0307d
34 changed files with 1060 additions and 623 deletions

View File

@ -245,7 +245,7 @@ gem 'gitlab-sidekiq-fetcher', path: 'vendor/gems/sidekiq-reliable-fetch', requir
gem 'fugit', '~> 1.8.1'
# HTTP requests
gem 'httparty', '~> 0.20.0'
gem 'httparty', '~> 0.21.0'
# Colored output to console
gem 'rainbow', '~> 3.0'

View File

@ -296,7 +296,7 @@
{"name":"http-accept","version":"1.7.0","platform":"ruby","checksum":"c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126"},
{"name":"http-cookie","version":"1.0.5","platform":"ruby","checksum":"73756d46c7dbdc7023deecdb8a171348ea95a1b99810b31cfe8b4fb4e9a6318f"},
{"name":"http-form_data","version":"2.3.0","platform":"ruby","checksum":"cc4eeb1361d9876821e31d7b1cf0b68f1cf874b201d27903480479d86448a5f3"},
{"name":"httparty","version":"0.20.0","platform":"ruby","checksum":"490d2a028a5accc611f1685d479d80ef80b129140d24a93c53c119f578614867"},
{"name":"httparty","version":"0.21.0","platform":"ruby","checksum":"00ef7bf9a71f30a3bff88edeb5b16a34bea883ab67c246b3f0db2d6794fe1214"},
{"name":"httpclient","version":"2.8.3","platform":"ruby","checksum":"2951e4991214464c3e92107e46438527d23048e634f3aee91c719e0bdfaebda6"},
{"name":"i18n","version":"1.12.0","platform":"ruby","checksum":"91e3cc1b97616d308707eedee413d82ee021d751c918661fb82152793e64aced"},
{"name":"i18n_data","version":"0.13.1","platform":"ruby","checksum":"e5aa99b09a69b463bb0443fc1f9540351a49f3d1541c5e91316bafa035c63f66"},

View File

@ -867,8 +867,8 @@ GEM
http-cookie (1.0.5)
domain_name (~> 0.5)
http-form_data (2.3.0)
httparty (0.20.0)
mime-types (~> 3.0)
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.12.0)
@ -1862,7 +1862,7 @@ DEPENDENCIES
health_check (~> 3.0)
html-pipeline (~> 2.14.3)
html2text
httparty (~> 0.20.0)
httparty (~> 0.21.0)
icalendar
invisible_captcha (~> 2.0.0)
ipaddr (~> 1.2.5)

View File

@ -0,0 +1,60 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import getMergeRequestPipelines from '~/pipelines/graphql/queries/get_merge_request_pipelines.query.graphql';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { getQueryHeaders } from '~/pipelines/components/graph/utils';
import { graphqlEtagMergeRequestPipelines } from '~/pipelines/utils';
export default {
components: {
GlLoadingIcon,
},
inject: ['graphqlPath', 'mergeRequestId', 'targetProjectFullPath'],
data() {
return {
pipelines: [],
};
},
apollo: {
pipelines: {
query: getMergeRequestPipelines,
context() {
return getQueryHeaders(this.graphqlResourceEtag);
},
pollInterval: 10000,
variables() {
return {
fullPath: this.targetProjectFullPath,
mergeRequestIid: String(this.mergeRequestId),
};
},
update(data) {
return data?.project?.mergeRequest?.pipelines?.nodes || [];
},
error() {
createAlert({ message: this.$options.i18n.fetchError });
},
},
},
computed: {
graphqlResourceEtag() {
return graphqlEtagMergeRequestPipelines(this.graphqlPath, this.mergeRequestId);
},
isLoading() {
return this.$apollo.queries.pipelines.loading;
},
},
i18n: {
fetchError: __("There was an error fetching this merge request's pipelines."),
},
};
</script>
<template>
<div class="gl-mt-3">
<gl-loading-icon v-if="isLoading" size="lg" />
<ul v-else>
<li v-for="pipeline in pipelines" :key="pipeline.id">{{ pipeline.path }}</li>
</ul>
</div>
</template>

View File

@ -93,7 +93,11 @@ function mountPipelines() {
const { mrWidgetData } = gl;
const table = new Vue({
components: {
CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
CommitPipelinesTable: () => {
return gon.features.mrPipelinesGraphql
? import('~/commit/pipelines/pipelines_table_wrapper.vue')
: import('~/commit/pipelines/pipelines_table.vue');
},
},
apolloProvider,
provide: {
@ -103,6 +107,8 @@ function mountPipelines() {
fullPath: pipelineTableViewEl.dataset.fullPath,
graphqlPath: pipelineTableViewEl.dataset.graphqlPath,
manualActionsLimit: 50,
mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
withFailedJobsDetails: true,
},
render(createElement) {

View File

@ -3,8 +3,9 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__, sprintf } from '~/locale';
import { getQueryHeaders } from '~/pipelines/components/graph/utils';
import { graphqlEtagPipelinePath } from '~/pipelines/utils';
import getPipelineFailedJobs from '../../../graphql/queries/get_pipeline_failed_jobs.query.graphql';
import { graphqlEtagPipelinePath, sortJobsByStatus } from './utils';
import { sortJobsByStatus } from './utils';
import FailedJobDetails from './failed_job_details.vue';
const POLL_INTERVAL = 10000;

View File

@ -13,7 +13,3 @@ export const sortJobsByStatus = (jobs = []) => {
return 1;
});
};
export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
return `${graphqlPath}pipelines/id/${pipelineId}`;
};

View File

@ -0,0 +1,16 @@
query getMergeRequestPipelines($mergeRequestIid: String!, $fullPath: ID!) {
project(fullPath: $fullPath) {
id
mergeRequest(iid: $mergeRequestIid) {
id
pipelines {
count
nodes {
id
iid
path
}
}
}
}
}

View File

@ -154,3 +154,11 @@ export const getPipelineDefaultTab = (url) => {
return null;
};
export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
return `${graphqlPath}pipelines/id/${pipelineId}`;
};
export const graphqlEtagMergeRequestPipelines = (graphqlPath, mergeRequestId) => {
return `${graphqlPath}merge_requests/id/${mergeRequestId}`;
};

View File

@ -1,5 +1,5 @@
<script>
import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import {
destroyUserCountsManager,
@ -34,20 +34,21 @@ export default {
),
SuperSidebarToggle,
BrandLogo,
GlIcon,
},
i18n: {
createNew: __('Create new...'),
homepage: __('Homepage'),
issues: __('Issues'),
mergeRequests: __('Merge requests'),
search: __('Search'),
searchKbdHelp: sprintf(
s__('GlobalSearch|Search GitLab %{kbdOpen}/%{kbdClose}'),
s__('GlobalSearch|Type %{kbdOpen}/%{kbdClose} to search'),
{ kbdOpen: '<kbd>', kbdClose: '</kbd>' },
false,
),
todoList: __('To-Do list'),
stopImpersonating: __('Stop impersonating'),
searchBtnText: __('Search or go to…'),
},
directives: {
GlTooltip: GlTooltipDirective,
@ -128,17 +129,6 @@ export default {
/>
<create-menu v-if="sidebarData.is_logged_in" :groups="sidebarData.create_new_menu_groups" />
<gl-button
id="super-sidebar-search"
v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
v-gl-modal="$options.SEARCH_MODAL_ID"
data-testid="super-sidebar-search-button"
icon="search"
:aria-label="$options.i18n.search"
category="tertiary"
/>
<search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
<user-menu v-if="sidebarData.is_logged_in" :data="sidebarData" />
<gl-button
@ -202,5 +192,18 @@ export default {
data-track-property="nav_core_menu"
/>
</div>
<div class="gl-px-3 gl-pt-2 gl-pb-3">
<button
id="super-sidebar-search"
v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
v-gl-modal="$options.SEARCH_MODAL_ID"
class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
data-testid="super-sidebar-search-button"
>
<gl-icon name="search" />
{{ $options.i18n.searchBtnText }}
</button>
<search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
</div>
</div>
</template>

View File

@ -50,6 +50,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:review_apps_redeploy_mr_widget, project)
push_frontend_feature_flag(:ci_job_failures_in_mr, project)
push_frontend_feature_flag(:action_cable_notes, project)
push_frontend_feature_flag(:mr_pipelines_graphql, project)
end
before_action only: [:edit] do

View File

@ -2,10 +2,6 @@
module LooseForeignKeys
class ModificationTracker
MAX_DELETES = 100_000
MAX_UPDATES = 50_000
MAX_RUNTIME = 30.seconds # must be less than the scheduling frequency of the LooseForeignKeys::CleanupWorker cron worker
delegate :monotonic_time, to: :'Gitlab::Metrics::System'
def initialize
@ -22,6 +18,18 @@ module LooseForeignKeys
)
end
def max_runtime
30.seconds
end
def max_deletes
100_000
end
def max_updates
50_000
end
def add_deletions(table, count)
@delete_count_by_table[table] += count
@deletes_counter.increment({ table: table }, count)
@ -33,9 +41,9 @@ module LooseForeignKeys
end
def over_limit?
@delete_count_by_table.values.sum >= MAX_DELETES ||
@update_count_by_table.values.sum >= MAX_UPDATES ||
monotonic_time - @start_time >= MAX_RUNTIME
@delete_count_by_table.values.sum >= max_deletes ||
@update_count_by_table.values.sum >= max_updates ||
monotonic_time - @start_time >= max_runtime
end
def stats

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module LooseForeignKeys
# This is a modification tracker with the additional limits that can be enabled
# for some database via an OPS Feature Flag.
class TurboModificationTracker < ModificationTracker
extend ::Gitlab::Utils::Override
override :max_runtime
def max_runtime
45.seconds
end
override :max_deletes
def max_deletes
200_000
end
override :max_updates
def max_updates
150_000
end
end
end

View File

@ -4,13 +4,13 @@ module LooseForeignKeys
class ProcessDeletedRecordsService
BATCH_SIZE = 1000
def initialize(connection:)
def initialize(connection:, modification_tracker: LooseForeignKeys::ModificationTracker.new)
@connection = connection
@modification_tracker = modification_tracker
end
def execute
raised_error = false
modification_tracker = ModificationTracker.new
tracked_tables.cycle do |table|
records = load_batch_for_table(table)
@ -54,7 +54,7 @@ module LooseForeignKeys
private
attr_reader :connection
attr_reader :connection, :modification_tracker
def db_config_name
::Gitlab::Database.db_config_name(connection)

View File

@ -12,18 +12,23 @@ module LooseForeignKeys
idempotent!
def perform
connection_name, base_model = current_connection_name_and_base_model
modification_tracker, turbo_mode = initialize_modification_tracker_for(connection_name)
# Add small buffer on MAX_RUNTIME to account for single long running
# query or extra worker time after the cleanup.
lock_ttl = ModificationTracker::MAX_RUNTIME + 20.seconds
lock_ttl = modification_tracker.max_runtime + 10.seconds
in_lock(self.class.name.underscore, ttl: lock_ttl, retries: 0) do
stats = {}
connection_name, base_model = current_connection_name_and_base_model
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
stats = ProcessDeletedRecordsService.new(connection: base_model.connection).execute
stats = ProcessDeletedRecordsService.new(
connection: base_model.connection,
modification_tracker: modification_tracker
).execute
stats[:connection] = connection_name
stats[:turbo_mode] = turbo_mode
end
log_extra_metadata_on_done(:stats, stats)
@ -41,5 +46,16 @@ module LooseForeignKeys
connections_with_name = Gitlab::Database.database_base_models_with_gitlab_shared.to_a # this will never be empty
connections_with_name[minutes_since_epoch % connections_with_name.count]
end
def initialize_modification_tracker_for(connection_name)
turbo_mode = turbo_mode?(connection_name)
modification_tracker ||= turbo_mode ? TurboModificationTracker.new : ModificationTracker.new
[modification_tracker, turbo_mode]
end
def turbo_mode?(connection_name)
%w[main ci].include?(connection_name) &&
Feature.enabled?(:"loose_foreign_keys_turbo_mode_#{connection_name}", type: :ops)
end
end
end

View File

@ -1,8 +1,8 @@
---
name: advanced_search_decrease_indexing_timeout
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111902
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391570
milestone: '15.9'
name: mr_pipelines_graphql
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128929
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419726
milestone: '16.4'
type: development
group: group::global search
default_enabled: true
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: loose_foreign_keys_turbo_mode_ci
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128538
rollout_issue_url:
milestone: '16.4'
type: ops
group: group::tenant scale
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: loose_foreign_keys_turbo_mode_main
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128538
rollout_issue_url:
milestone: '16.4'
type: ops
group: group::tenant scale
default_enabled: false

View File

@ -193,7 +193,7 @@ This new architecture allows GitLab to be resilient to connectivity issues betwe
WARNING:
This list of limitations only reflects the latest version of GitLab. If you are using an older version, extra limitations may be in place.
- Pushing directly to a **secondary** site redirects (for HTTP) or proxies (for SSH) the request to the **primary** site instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/-/issues/1381), except when using Git over HTTP with credentials embedded in the URI. For example, `https://user:password@secondary.tld`.
- Pushing directly to a **secondary** site redirects (for HTTP) or proxies (for SSH) the request to the **primary** site instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/-/issues/1381). The limitation is that you cannot use Git over HTTP with credentials embedded in the URI, for example, `https://user:personal-access-token@secondary.tld`. For more information, see how to [use a Geo Site](replication/usage.md).
- The **primary** site has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** site to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/-/issues/208465).
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. Consider using [the GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit) to deploy and operate production GitLab instances based on our [Reference Architectures](../reference_architectures/index.md), including automation of common daily tasks. We are planning to [improve Geo's installation even further](https://gitlab.com/groups/gitlab-org/-/epics/1465).
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** site.

View File

@ -339,6 +339,12 @@ It points to the commit you're rolling back to.
For the rollback to succeed, the deployment process must be defined in
the job's `script`.
Only the [deployment jobs](../jobs/index.md#deployment-jobs) are run.
In cases where a previous job generates artifacts that must be regenerated
on deploy, you must manually run the necessary jobs from the pipelines page.
For example, if you use Terraform and your `plan` and `apply` commands are separated
into multiple jobs, you must manually run the jobs to deploy or roll back.
#### Retry or roll back a deployment
If there is a problem with a deployment, you can retry it or roll it back.

View File

@ -120,7 +120,7 @@ To install `agentk`:
apiVersion: v1
kind: Secret
metadata:
name: gitlab-agent-token
name: gitlab-agent-token-initial
type: Opaque
stringData:
values.yaml: |-
@ -175,10 +175,12 @@ To install `agentk`:
kasAddress: "wss://kas.gitlab.com"
valuesFrom:
- kind: Secret
name: gitlab-agent-token
name: gitlab-agent-token-initial
valuesKey: values.yaml
```
The Helm release creates a new secret with the name `gitlab-agent-token`, which is managed by Helm.
1. To verify that `agentk` is installed and running in the cluster, run the following command:
```shell

View File

@ -21829,9 +21829,6 @@ msgstr ""
msgid "GlobalSearch|Search GitLab"
msgstr ""
msgid "GlobalSearch|Search GitLab %{kbdOpen}/%{kbdClose}"
msgstr ""
msgid "GlobalSearch|Search for projects, issues, etc."
msgstr ""
@ -21862,6 +21859,9 @@ msgstr ""
msgid "GlobalSearch|There was an error fetching the \"Syntax Options\" document."
msgstr ""
msgid "GlobalSearch|Type %{kbdOpen}/%{kbdClose} to search"
msgstr ""
msgid "GlobalSearch|Type and press the enter key to submit search."
msgstr ""
@ -41804,6 +41804,9 @@ msgstr ""
msgid "Search or filter results…"
msgstr ""
msgid "Search or go to…"
msgstr ""
msgid "Search page"
msgstr ""
@ -47906,6 +47909,9 @@ msgstr ""
msgid "There was an error fetching the variables."
msgstr ""
msgid "There was an error fetching this merge request's pipelines."
msgstr ""
msgid "There was an error fetching value stream analytics stages."
msgstr ""

View File

@ -7,7 +7,7 @@
# them to be overridden.
#
# For details on how to run this, see the documentation comments at the top of
# qa/qa/specs/features/ee/browser_ui/3_create/remote_development/create_new_workspace_and_terminate_spec.rb
# qa/qa/specs/features/ee/browser_ui/3_create/remote_development/with_prerequisite_done/workspace_actions_with_prerequisite_done_spec.rb
DEFAULT_PASSWORD='5iveL!fe'
@ -28,9 +28,8 @@ echo "TEST_INSTANCE_URL: ${TEST_INSTANCE_URL}"
working_directory="$(git rev-parse --show-toplevel)/qa"
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/397005
# Remove the '--tag quarantine' from below once this test is removed from quarantine
# This test is currently quarantined as its only used for local testing, so we have to use the '--tag quarantine'
(cd "$working_directory" && \
bundle && \
bundle exec bin/qa Test::Instance::All "$TEST_INSTANCE_URL" -- \
--tag quarantine qa/specs/features/ee/browser_ui/3_create/remote_development/without_setup)
--tag quarantine qa/specs/features/ee/browser_ui/3_create/remote_development/with_prerequisite_done/workspace_actions_with_prerequisite_done_spec.rb)

View File

@ -48,60 +48,29 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
# rubocop:enable RSpec/AvoidConditionalStatements
context 'when a user created a merge request in the parent project' do
let!(:merge_request) do
create(
:merge_request,
source_project: project,
target_project: project,
source_branch: 'feature',
target_branch: 'master'
)
end
let!(:push_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
end
context 'with feature flag `mr_pipelines_graphql turned off`' do
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
stub_feature_flags(mr_pipelines_graphql: false)
end
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
context 'when a user created a merge request in the parent project' do
let!(:merge_request) do
create(
:merge_request,
source_project: project,
target_project: project,
source_branch: 'feature',
target_branch: 'master'
)
end
end
it 'sees the latest detached merge request pipeline as the head pipeline', :sidekiq_might_not_need_inline do
click_link "Overview"
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
context 'when a user updated a merge request in the parent project', :sidekiq_might_not_need_inline do
let!(:push_pipeline_2) do
let!(:push_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline_2) do
let!(:detached_merge_request_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
@ -117,49 +86,186 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}")
expect(all('[data-testid="pipeline-url-link"]')[2])
.to have_content("##{push_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}")
expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
it 'sees detached tag for detached merge request pipelines' do
page.within('.ci-table') do
expect(all('.pipeline-tags')[0])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[1])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[2])
.not_to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[3])
.not_to have_content(expected_detached_mr_tag)
end
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
click_link 'Overview'
it 'sees the latest detached merge request pipeline as the head pipeline', :sidekiq_might_not_need_inline do
click_link "Overview"
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
context 'when a user updated a merge request in the parent project', :sidekiq_might_not_need_inline do
let!(:push_pipeline_2) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline_2) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
end
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}")
expect(all('[data-testid="pipeline-url-link"]')[2])
.to have_content("##{push_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}")
end
end
it 'sees detached tag for detached merge request pipelines' do
page.within('.ci-table') do
expect(all('.pipeline-tags')[0])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[1])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[2])
.not_to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[3])
.not_to have_content(expected_detached_mr_tag)
end
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
click_link 'Overview'
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
end
end
end
context 'when a user created a merge request in the parent project' do
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
context 'when a user merges a merge request in the parent project', :sidekiq_might_not_need_inline do
before do
click_link 'Overview'
click_button 'Set to auto-merge'
wait_for_requests
end
context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do
expect(page).to have_content mr_widget_title
expect(page).to have_button('Cancel auto-merge')
end
end
context 'when branch pipeline succeeds' do
before do
click_link 'Overview'
push_pipeline.reload.succeed!
wait_for_requests
end
it 'waits the head pipeline' do
expect(page).to have_content mr_widget_title
expect(page).to have_button('Cancel auto-merge')
end
end
end
end
context 'when there are no `merge_requests` keyword in .gitlab-ci.yml' do
let(:config) do
{
build: {
script: 'build'
},
test: {
script: 'test'
},
deploy: {
script: 'deploy'
}
}
end
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 1)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end
end
it 'sees the latest branch pipeline as the head pipeline', :sidekiq_might_not_need_inline do
click_link 'Overview'
page.within('.ci-widget-content') do
expect(page).to have_content("##{push_pipeline.id}")
end
end
end
end
context 'when a user created a merge request in the parent project' do
context 'when a user created a merge request from a forked project to the parent project', :sidekiq_might_not_need_inline do
let(:merge_request) do
create(
:merge_request,
source_project: forked_project,
target_project: project,
source_branch: 'feature',
target_branch: 'master'
)
end
let!(:push_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
end
let(:forked_project) { fork_project(project, user2, repository: true) }
let(:user2) { create(:user) }
before do
forked_project.add_maintainer(user2)
stub_feature_flags(auto_merge_labels_mr_widget: false)
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
@ -167,9 +273,133 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
end
context 'when a user merges a merge request in the parent project', :sidekiq_might_not_need_inline do
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
click_link "Overview"
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
let!(:push_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
end
before do
click_link 'Overview'
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}")
expect(all('[data-testid="pipeline-url-link"]')[2])
.to have_content("##{push_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}")
end
end
it 'sees detached tag for detached merge request pipelines' do
page.within('.ci-table') do
expect(all('.pipeline-tags')[0])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[1])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[2])
.not_to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[3])
.not_to have_content(expected_detached_mr_tag)
end
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
click_link "Overview"
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
end
end
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
end
end
context 'when the latest pipeline is running in the parent project' do
before do
create(:ci_pipeline,
source: :merge_request_event,
project: project,
ref: 'feature',
sha: merge_request.diff_head_sha,
user: user,
merge_request: merge_request,
status: :running)
merge_request.update_head_pipeline
end
context 'when the previous pipeline failed in the fork project' do
before do
detached_merge_request_pipeline.reload.drop!
end
context 'when the parent project enables pipeline must succeed' do
before do
project.update!(only_allow_merge_if_pipeline_succeeds: true)
end
it 'shows Set to auto-merge button' do
visit project_merge_request_path(project, merge_request)
expect(page).to have_button('Set to auto-merge')
end
end
end
end
context 'when a user merges a merge request from a forked project to the parent project' do
before do
click_link("Overview")
click_button 'Set to auto-merge'
wait_for_requests
@ -182,9 +412,21 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
end
context 'when detached merge request pipeline succeeds' do
before do
detached_merge_request_pipeline.reload.succeed!
refresh
end
it 'merges the merge request' do
expect(page).to have_content('Merged by')
expect(page).to have_button('Revert')
end
end
context 'when branch pipeline succeeds' do
before do
click_link 'Overview'
push_pipeline.reload.succeed!
wait_for_requests
@ -197,241 +439,5 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
end
end
end
context 'when there are no `merge_requests` keyword in .gitlab-ci.yml' do
let(:config) do
{
build: {
script: 'build'
},
test: {
script: 'test'
},
deploy: {
script: 'deploy'
}
}
end
it 'sees a branch pipeline in pipeline tab' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-created"]', count: 1)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{push_pipeline.id}")
end
end
it 'sees the latest branch pipeline as the head pipeline', :sidekiq_might_not_need_inline do
click_link 'Overview'
page.within('.ci-widget-content') do
expect(page).to have_content("##{push_pipeline.id}")
end
end
end
end
context 'when a user created a merge request from a forked project to the parent project', :sidekiq_might_not_need_inline do
let(:merge_request) do
create(
:merge_request,
source_project: forked_project,
target_project: project,
source_branch: 'feature',
target_branch: 'master'
)
end
let!(:push_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
end
let(:forked_project) { fork_project(project, user2, repository: true) }
let(:user2) { create(:user) }
before do
forked_project.add_maintainer(user2)
stub_feature_flags(auto_merge_labels_mr_widget: false)
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
expect(first('[data-testid="pipeline-url-link"]')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
click_link "Overview"
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 2)
end
context 'when a user updated a merge request from a forked project to the parent project' do
let!(:push_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:push)
.payload
end
let!(:detached_merge_request_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
.execute(:merge_request_event, merge_request: merge_request)
.payload
end
before do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
end
it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
expect(all('[data-testid="pipeline-url-link"]')[0])
.to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[1])
.to have_content("##{detached_merge_request_pipeline.id}")
expect(all('[data-testid="pipeline-url-link"]')[2])
.to have_content("##{push_pipeline_2.id}")
expect(all('[data-testid="pipeline-url-link"]')[3])
.to have_content("##{push_pipeline.id}")
end
end
it 'sees detached tag for detached merge request pipelines' do
page.within('.ci-table') do
expect(all('.pipeline-tags')[0])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[1])
.to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[2])
.not_to have_content(expected_detached_mr_tag)
expect(all('.pipeline-tags')[3])
.not_to have_content(expected_detached_mr_tag)
end
end
it 'sees the latest detached merge request pipeline as the head pipeline' do
click_link "Overview"
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
end
end
it 'sees pipeline list in forked project' do
visit project_pipelines_path(forked_project)
expect(page).to have_selector('[data-testid="ci-badge-pending"]', count: 4)
end
end
context 'when the latest pipeline is running in the parent project' do
before do
create(:ci_pipeline,
source: :merge_request_event,
project: project,
ref: 'feature',
sha: merge_request.diff_head_sha,
user: user,
merge_request: merge_request,
status: :running)
merge_request.update_head_pipeline
end
context 'when the previous pipeline failed in the fork project' do
before do
detached_merge_request_pipeline.reload.drop!
end
context 'when the parent project enables pipeline must succeed' do
before do
project.update!(only_allow_merge_if_pipeline_succeeds: true)
end
it 'shows Set to auto-merge button' do
visit project_merge_request_path(project, merge_request)
expect(page).to have_button('Set to auto-merge')
end
end
end
end
context 'when a user merges a merge request from a forked project to the parent project' do
before do
click_link("Overview")
click_button 'Set to auto-merge'
wait_for_requests
end
context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do
expect(page).to have_content mr_widget_title
expect(page).to have_button('Cancel auto-merge')
end
end
context 'when detached merge request pipeline succeeds' do
before do
detached_merge_request_pipeline.reload.succeed!
refresh
end
it 'merges the merge request' do
expect(page).to have_content('Merged by')
expect(page).to have_button('Revert')
end
end
context 'when branch pipeline succeeds' do
before do
push_pipeline.reload.succeed!
wait_for_requests
end
it 'waits the head pipeline' do
expect(page).to have_content mr_widget_title
expect(page).to have_button('Cancel auto-merge')
end
end
end
end
end

View File

@ -30,15 +30,33 @@ RSpec.describe 'Merge request > User sees pipelines from forked project', :js,
before do
create(:ci_build, pipeline: pipeline, name: 'rspec')
create(:ci_build, pipeline: pipeline, name: 'spinach')
sign_in(user)
visit project_merge_request_path(target_project, merge_request)
end
it 'user visits a pipelines page', :sidekiq_might_not_need_inline do
page.within('.merge-request-tabs') { click_link 'Pipelines' }
context 'with feature flag `mr_pipelines_graphql` turned off' do
before do
stub_feature_flags(mr_pipelines_graphql: false)
visit project_merge_request_path(target_project, merge_request)
end
it 'user visits a pipelines page', :sidekiq_might_not_need_inline do
page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('.ci-table') do
expect(page).to have_content(pipeline.id)
end
end
end
context 'with feature flag `mr_pipelines_graphql` turned on' do
before do
stub_feature_flags(mr_pipelines_graphql: true)
visit project_merge_request_path(target_project, merge_request)
end
it 'user visits a pipelines page', :sidekiq_might_not_need_inline do
page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('.ci-table') do
expect(page).to have_content(pipeline.id)
end
end

View File

@ -3,285 +3,291 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees pipelines', :js, feature_category: :code_review_workflow do
describe 'pipeline tab' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
let(:user) { project.creator }
context 'with feature flag `mr_pipelines_graphql turned off`' do
before do
project.add_maintainer(user)
sign_in(user)
stub_feature_flags(mr_pipelines_graphql: false)
end
context 'with pipelines' do
let!(:pipeline) do
describe 'pipeline tab' do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.target_project }
let(:user) { project.creator }
before do
project.add_maintainer(user)
sign_in(user)
end
context 'with pipelines' do
let!(:pipeline) do
create(
:ci_pipeline,
:success,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
end
let!(:manual_job) { create(:ci_build, :manual, name: 'job1', stage: 'deploy', pipeline: pipeline) }
let!(:job) { create(:ci_build, :success, name: 'job2', stage: 'test', pipeline: pipeline) }
before do
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
it 'pipelines table displays correctly' do
visit project_merge_request_path(project, merge_request)
expect(page.find('.ci-widget')).to have_content('passed')
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
wait_for_requests
page.within(find('[data-testid="pipeline-table-row"]', match: :first)) do
expect(page).to have_selector('[data-testid="ci-badge-passed"]')
expect(page).to have_content(pipeline.id)
expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
expect(page).to have_css('[data-testid="pipelines-manual-actions-dropdown"]')
expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
end
end
context 'with a detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
it 'displays the "Run pipeline" button' do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
wait_for_requests
expect(page.find('[data-testid="run_pipeline_button"]')).to have_text('Run pipeline')
end
end
context 'with a merged results pipeline' do
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
it 'displays the "Run pipeline" button' do
visit project_merge_request_path(project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
wait_for_requests
expect(page.find('[data-testid="run_pipeline_button"]')).to have_text('Run pipeline')
end
end
end
context 'without pipelines' do
before do
visit project_merge_request_path(project, merge_request)
end
it 'user visits merge request page' do
page.within('.merge-request-tabs') do
expect(page).to have_link('Pipelines')
end
end
it 'shows empty state with run pipeline button' do
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
expect(page).to have_content('There are currently no pipelines.')
expect(page.find('[data-testid="run_pipeline_button"]')).to have_text('Run pipeline')
end
end
end
describe 'fork MRs in parent project', :sidekiq_inline do
include ProjectForksHelper
let_it_be(:parent_project) { create(:project, :public, :repository) }
let_it_be(:forked_project) { fork_project(parent_project, developer_in_fork, repository: true, target_project: create(:project, :public, :repository)) }
let_it_be(:developer_in_parent) { create(:user) }
let_it_be(:developer_in_fork) { create(:user) }
let_it_be(:reporter_in_parent_and_developer_in_fork) { create(:user) }
let(:merge_request) do
create(
:ci_pipeline,
:success,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
:merge_request,
:with_detached_merge_request_pipeline,
source_project: forked_project,
source_branch: 'feature',
target_project: parent_project,
target_branch: 'master'
)
end
let!(:manual_job) { create(:ci_build, :manual, name: 'job1', stage: 'deploy', pipeline: pipeline) }
let(:config) do
{ test: { script: 'test', rules: [{ if: '$CI_MERGE_REQUEST_ID' }] } }
end
let!(:job) { create(:ci_build, :success, name: 'job2', stage: 'test', pipeline: pipeline) }
before_all do
parent_project.add_developer(developer_in_parent)
parent_project.add_reporter(reporter_in_parent_and_developer_in_fork)
forked_project.add_developer(developer_in_fork)
forked_project.add_developer(reporter_in_parent_and_developer_in_fork)
end
before do
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
stub_ci_pipeline_yaml_file(YAML.dump(config))
sign_in(actor)
end
it 'pipelines table displays correctly' do
visit project_merge_request_path(project, merge_request)
after do
parent_project.all_pipelines.delete_all
forked_project.all_pipelines.delete_all
end
expect(page.find('.ci-widget')).to have_content('passed')
context 'when actor is a developer in parent project' do
let(:actor) { developer_in_parent }
page.within('.merge-request-tabs') do
click_link('Pipelines')
it 'creates a pipeline in the parent project when user proceeds with the warning' do
visit project_merge_request_path(parent_project, merge_request)
create_merge_request_pipeline
act_on_security_warning(action: 'Run pipeline')
check_pipeline(expected_project: parent_project)
check_head_pipeline(expected_project: parent_project)
end
wait_for_requests
it 'does not create a pipeline in the parent project when user cancels the action', :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state do
visit project_merge_request_path(parent_project, merge_request)
page.within(find('[data-testid="pipeline-table-row"]', match: :first)) do
expect(page).to have_selector('[data-testid="ci-badge-passed"]')
expect(page).to have_content(pipeline.id)
expect(page).to have_content('API')
expect(page).to have_css('[data-testid="pipeline-mini-graph"]')
expect(page).to have_css('[data-testid="pipelines-manual-actions-dropdown"]')
expect(page).to have_css('[data-testid="pipeline-multi-actions-dropdown"]')
create_merge_request_pipeline
act_on_security_warning(action: 'Cancel')
check_no_new_pipeline_created
end
end
context 'with a detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
context 'when actor is a developer in fork project' do
let(:actor) { developer_in_fork }
it 'displays the "Run pipeline" button' do
visit project_merge_request_path(project, merge_request)
it 'creates a pipeline in the fork project' do
visit project_merge_request_path(parent_project, merge_request)
page.within('.merge-request-tabs') do
click_link('Pipelines')
create_merge_request_pipeline
check_pipeline(expected_project: forked_project)
check_head_pipeline(expected_project: forked_project)
end
end
context 'when actor is a reporter in parent project and a developer in fork project' do
let(:actor) { reporter_in_parent_and_developer_in_fork }
it 'creates a pipeline in the fork project' do
visit project_merge_request_path(parent_project, merge_request)
create_merge_request_pipeline
check_pipeline(expected_project: forked_project)
check_head_pipeline(expected_project: forked_project)
end
end
def create_merge_request_pipeline
page.within('.merge-request-tabs') { click_link('Pipelines') }
click_on('Run pipeline')
end
def check_pipeline(expected_project:)
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="pipeline-table-row"]', count: 4)
page.within(first('[data-testid="pipeline-table-row"]')) do
page.within('.pipeline-tags') do
expect(page.find('[data-testid="pipeline-url-link"]')[:href]).to include(expected_project.full_path)
expect(page).to have_content('merge request')
end
page.within('.pipeline-triggerer') do
expect(page).to have_link(href: user_path(actor))
end
end
wait_for_requests
expect(page.find('[data-testid="run_pipeline_button"]')).to have_text('Run pipeline')
end
end
context 'with a merged results pipeline' do
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
def check_head_pipeline(expected_project:)
page.within('.merge-request-tabs') { click_link('Overview') }
it 'displays the "Run pipeline" button' do
visit project_merge_request_path(project, merge_request)
page.within('.ci-widget-content') do
expect(page.find('.pipeline-id')[:href]).to include(expected_project.full_path)
end
end
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
def act_on_security_warning(action:)
page.within('#create-pipeline-for-fork-merge-request-modal') do
expect(page).to have_content('Are you sure you want to run this pipeline?')
click_button(action)
end
end
wait_for_requests
expect(page.find('[data-testid="run_pipeline_button"]')).to have_text('Run pipeline')
def check_no_new_pipeline_created
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="pipeline-table-row"]', count: 2)
end
end
end
context 'without pipelines' do
describe 'race condition' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:build_push_data) { { ref: 'feature', checkout_sha: TestEnv::BRANCH_SHA['feature'] } }
let(:merge_request_params) do
{ "source_branch" => "feature", "source_project_id" => project.id,
"target_branch" => "master", "target_project_id" => project.id, "title" => "A" }
end
before do
visit project_merge_request_path(project, merge_request)
project.add_maintainer(user)
sign_in user
end
it 'user visits merge request page' do
page.within('.merge-request-tabs') do
expect(page).to have_link('Pipelines')
end
end
context 'when pipeline and merge request were created simultaneously', :delete do
before do
stub_ci_pipeline_to_return_yaml_file
it 'shows empty state with run pipeline button' do
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
threads = []
expect(page).to have_content('There are currently no pipelines.')
expect(page.find('[data-testid="run_pipeline_button"]')).to have_text('Run pipeline')
end
end
end
describe 'fork MRs in parent project', :sidekiq_inline do
include ProjectForksHelper
let_it_be(:parent_project) { create(:project, :public, :repository) }
let_it_be(:forked_project) { fork_project(parent_project, developer_in_fork, repository: true, target_project: create(:project, :public, :repository)) }
let_it_be(:developer_in_parent) { create(:user) }
let_it_be(:developer_in_fork) { create(:user) }
let_it_be(:reporter_in_parent_and_developer_in_fork) { create(:user) }
let(:merge_request) do
create(
:merge_request,
:with_detached_merge_request_pipeline,
source_project: forked_project,
source_branch: 'feature',
target_project: parent_project,
target_branch: 'master'
)
end
let(:config) do
{ test: { script: 'test', rules: [{ if: '$CI_MERGE_REQUEST_ID' }] } }
end
before_all do
parent_project.add_developer(developer_in_parent)
parent_project.add_reporter(reporter_in_parent_and_developer_in_fork)
forked_project.add_developer(developer_in_fork)
forked_project.add_developer(reporter_in_parent_and_developer_in_fork)
end
before do
stub_ci_pipeline_yaml_file(YAML.dump(config))
sign_in(actor)
end
after do
parent_project.all_pipelines.delete_all
forked_project.all_pipelines.delete_all
end
context 'when actor is a developer in parent project' do
let(:actor) { developer_in_parent }
it 'creates a pipeline in the parent project when user proceeds with the warning' do
visit project_merge_request_path(parent_project, merge_request)
create_merge_request_pipeline
act_on_security_warning(action: 'Run pipeline')
check_pipeline(expected_project: parent_project)
check_head_pipeline(expected_project: parent_project)
end
it 'does not create a pipeline in the parent project when user cancels the action', :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state do
visit project_merge_request_path(parent_project, merge_request)
create_merge_request_pipeline
act_on_security_warning(action: 'Cancel')
check_no_new_pipeline_created
end
end
context 'when actor is a developer in fork project' do
let(:actor) { developer_in_fork }
it 'creates a pipeline in the fork project' do
visit project_merge_request_path(parent_project, merge_request)
create_merge_request_pipeline
check_pipeline(expected_project: forked_project)
check_head_pipeline(expected_project: forked_project)
end
end
context 'when actor is a reporter in parent project and a developer in fork project' do
let(:actor) { reporter_in_parent_and_developer_in_fork }
it 'creates a pipeline in the fork project' do
visit project_merge_request_path(parent_project, merge_request)
create_merge_request_pipeline
check_pipeline(expected_project: forked_project)
check_head_pipeline(expected_project: forked_project)
end
end
def create_merge_request_pipeline
page.within('.merge-request-tabs') { click_link('Pipelines') }
click_on('Run pipeline')
end
def check_pipeline(expected_project:)
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="pipeline-table-row"]', count: 4)
page.within(first('[data-testid="pipeline-table-row"]')) do
page.within('.pipeline-tags') do
expect(page.find('[data-testid="pipeline-url-link"]')[:href]).to include(expected_project.full_path)
expect(page).to have_content('merge request')
threads << Thread.new do
Sidekiq::Worker.skipping_transaction_check do
@merge_request = MergeRequests::CreateService.new(project: project, current_user: user, params: merge_request_params).execute
end
end
page.within('.pipeline-triggerer') do
expect(page).to have_link(href: user_path(actor))
threads << Thread.new do
Sidekiq::Worker.skipping_transaction_check do
@pipeline = Ci::CreatePipelineService.new(project, user, build_push_data).execute(:push).payload
end
end
end
end
end
def check_head_pipeline(expected_project:)
page.within('.merge-request-tabs') { click_link('Overview') }
page.within('.ci-widget-content') do
expect(page.find('.pipeline-id')[:href]).to include(expected_project.full_path)
end
end
def act_on_security_warning(action:)
page.within('#create-pipeline-for-fork-merge-request-modal') do
expect(page).to have_content('Are you sure you want to run this pipeline?')
click_button(action)
end
end
def check_no_new_pipeline_created
page.within('.ci-table') do
expect(page).to have_selector('[data-testid="pipeline-table-row"]', count: 2)
end
end
end
describe 'race condition' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:build_push_data) { { ref: 'feature', checkout_sha: TestEnv::BRANCH_SHA['feature'] } }
let(:merge_request_params) do
{ "source_branch" => "feature", "source_project_id" => project.id,
"target_branch" => "master", "target_project_id" => project.id, "title" => "A" }
end
before do
project.add_maintainer(user)
sign_in user
end
context 'when pipeline and merge request were created simultaneously', :delete do
before do
stub_ci_pipeline_to_return_yaml_file
threads = []
threads << Thread.new do
Sidekiq::Worker.skipping_transaction_check do
@merge_request = MergeRequests::CreateService.new(project: project, current_user: user, params: merge_request_params).execute
end
threads.each { |thr| thr.join }
end
threads << Thread.new do
Sidekiq::Worker.skipping_transaction_check do
@pipeline = Ci::CreatePipelineService.new(project, user, build_push_data).execute(:push).payload
end
it 'user sees pipeline in merge request widget', :sidekiq_might_not_need_inline do
visit project_merge_request_path(project, @merge_request)
expect(page.find(".ci-widget")).to have_content(TestEnv::BRANCH_SHA['feature'])
expect(page.find(".ci-widget")).to have_content("##{@pipeline.id}")
end
threads.each { |thr| thr.join }
end
it 'user sees pipeline in merge request widget', :sidekiq_might_not_need_inline do
visit project_merge_request_path(project, @merge_request)
expect(page.find(".ci-widget")).to have_content(TestEnv::BRANCH_SHA['feature'])
expect(page.find(".ci-widget")).to have_content("##{@pipeline.id}")
end
end
end

View File

@ -291,3 +291,34 @@ export const refsListPropsMock = {
urlPart: '/some/project/-/commits/',
refType: 'heads',
};
const createMergeRequestPipelines = (count = 30) => {
const pipelines = [];
for (let i = 0; i < count; i += 1) {
pipelines.push({
id: i,
iid: i + 10,
path: `/project/pipelines/${i}`,
});
}
return {
count,
nodes: pipelines,
};
};
export const mergeRequestPipelinesResponse = {
data: {
project: {
__typename: 'Project',
id: 'gid://gitlab/Project/1',
mergeRequest: {
__typename: 'MergeRequest',
id: 'gid://gitlab/MergeRequest/1',
pipelines: createMergeRequestPipelines(),
},
},
},
};

View File

@ -0,0 +1,117 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import PipelinesTableWrapper from '~/commit/pipelines/pipelines_table_wrapper.vue';
import getMergeRequestsPipelines from '~/pipelines/graphql/queries/get_merge_request_pipelines.query.graphql';
import { mergeRequestPipelinesResponse } from '../mock_data';
Vue.use(VueApollo);
jest.mock('~/alert');
const pipelinesLength = mergeRequestPipelinesResponse.data.project.mergeRequest.pipelines.count;
let wrapper;
let mergeRequestPipelinesRequest;
let apolloMock;
const defaultProvide = {
graphqlPath: '/api/graphql/',
mergeRequestId: 1,
targetProjectFullPath: '/group/project',
};
const createComponent = () => {
const handlers = [[getMergeRequestsPipelines, mergeRequestPipelinesRequest]];
apolloMock = createMockApollo(handlers);
wrapper = shallowMount(PipelinesTableWrapper, {
apolloProvider: apolloMock,
provide: {
...defaultProvide,
},
});
return waitForPromises();
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineList = () => wrapper.findAll('li');
beforeEach(() => {
mergeRequestPipelinesRequest = jest.fn();
mergeRequestPipelinesRequest.mockResolvedValue(mergeRequestPipelinesResponse);
});
afterEach(() => {
apolloMock = null;
createAlert.mockClear();
});
describe('PipelinesTableWrapper component', () => {
describe('When queries are loading', () => {
beforeEach(() => {
createComponent();
});
it('renders the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not render the pipeline list', () => {
expect(findPipelineList()).toHaveLength(0);
});
});
describe('When there is an error fetching pipelines', () => {
beforeEach(async () => {
mergeRequestPipelinesRequest.mockRejectedValueOnce({ error: 'API error message' });
await createComponent();
});
it('shows an error message', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: "There was an error fetching this merge request's pipelines.",
});
});
});
describe('When queries have loaded', () => {
beforeEach(async () => {
await createComponent();
});
it('does not render the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('renders a pipeline list', () => {
expect(findPipelineList()).toHaveLength(pipelinesLength);
});
});
describe('polling', () => {
beforeEach(async () => {
await createComponent();
});
it('polls every 10 seconds', () => {
expect(mergeRequestPipelinesRequest).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(5000);
expect(mergeRequestPipelinesRequest).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(5000);
expect(mergeRequestPipelinesRequest).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -165,7 +165,7 @@ describe('UserBar component', () => {
it('search button should have tooltip', () => {
const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
it('should render search modal', () => {
@ -184,7 +184,7 @@ describe('UserBar component', () => {
findSearchModal().vm.$emit('hidden');
await nextTick();
const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
});
});

View File

@ -35,11 +35,14 @@ RSpec.describe Gitlab::HTTP do
super do |response|
response.instance_eval do
def read_body(*)
@body.each do |fragment|
mock_stream = @body.split(' ')
mock_stream.each do |fragment|
sleep 0.002.seconds
yield fragment if block_given?
end
@body
end
end
@ -64,8 +67,8 @@ RSpec.describe Gitlab::HTTP do
before do
stub_const("#{described_class}::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds)
WebMock.stub_request(:post, /.*/).to_return do |request|
{ body: %w(a b), status: 200 }
WebMock.stub_request(:post, /.*/).to_return do
{ body: "chunk-1 chunk-2", status: 200 }
end
end

View File

@ -2,12 +2,12 @@
require 'spec_helper'
RSpec.describe LooseForeignKeys::ModificationTracker do
RSpec.describe LooseForeignKeys::ModificationTracker, feature_category: :database do
subject(:tracker) { described_class.new }
describe '#over_limit?' do
it 'is true when deletion MAX_DELETES is exceeded' do
stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 5)
it 'is true when deletion max_deletes is exceeded' do
expect(tracker).to receive(:max_deletes).and_return(5)
tracker.add_deletions('issues', 10)
expect(tracker).to be_over_limit
@ -20,7 +20,7 @@ RSpec.describe LooseForeignKeys::ModificationTracker do
end
it 'is true when deletion MAX_UPDATES is exceeded' do
stub_const('LooseForeignKeys::ModificationTracker::MAX_UPDATES', 5)
expect(tracker).to receive(:max_updates).and_return(5)
tracker.add_updates('issues', 3)
tracker.add_updates('issues', 4)
@ -36,9 +36,11 @@ RSpec.describe LooseForeignKeys::ModificationTracker do
it 'is true when max runtime is exceeded' do
monotonic_time_before = 1 # this will be the start time
monotonic_time_after = described_class::MAX_RUNTIME.to_i + 1 # this will be returned when over_limit? is called
monotonic_time_after = 31 # this will be returned when over_limit? is called
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
expect(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(
monotonic_time_before, monotonic_time_after
)
tracker

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LooseForeignKeys::TurboModificationTracker, feature_category: :database do
subject(:tracker) { described_class.new }
let(:normal_tracker) { LooseForeignKeys::ModificationTracker.new }
context 'with limits should be higher than LooseForeignKeys::ModificationTracker' do
it 'expect max_deletes to be equal or higher' do
expect(tracker.max_deletes).to be >= normal_tracker.max_deletes
end
it 'expect max_updates to be equal or higher' do
expect(tracker.max_updates).to be >= normal_tracker.max_updates
end
it 'expect max_runtime to be equal or higher' do
expect(tracker.max_runtime).to be >= normal_tracker.max_runtime
end
end
end

View File

@ -162,7 +162,9 @@ RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService, feature_category:
end
before do
stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2)
allow_next_instance_of(LooseForeignKeys::ModificationTracker) do |instance|
allow(instance).to receive(:max_deletes).and_return(2)
end
stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1)
end

View File

@ -166,4 +166,64 @@ RSpec.describe LooseForeignKeys::CleanupWorker, feature_category: :cell do
end
end
end
describe 'turbo mode' do
context 'when turbo mode is off' do
where(:database_name, :feature_flag) do
:main | :loose_foreign_keys_turbo_mode_main
:ci | :loose_foreign_keys_turbo_mode_ci
end
with_them do
before do
stub_feature_flags(feature_flag => false)
end
it 'does not use TurboModificationTracker' do
allow_next_instance_of(LooseForeignKeys::TurboModificationTracker) do |instance|
expect(instance).not_to receive(:over_limit?)
end
perform_for(db: database_name)
end
it 'logs not using turbo mode' do
expect_next_instance_of(LooseForeignKeys::CleanupWorker) do |instance|
expect(instance).to receive(:log_extra_metadata_on_done).with(:stats, a_hash_including(turbo_mode: false))
end
perform_for(db: database_name)
end
end
end
context 'when turbo mode is on' do
where(:database_name, :feature_flag) do
:main | :loose_foreign_keys_turbo_mode_main
:ci | :loose_foreign_keys_turbo_mode_ci
end
with_them do
before do
stub_feature_flags(feature_flag => true)
end
it 'does not use TurboModificationTracker' do
expect_next_instance_of(LooseForeignKeys::TurboModificationTracker) do |instance|
expect(instance).to receive(:over_limit?).at_least(:once)
end
perform_for(db: database_name)
end
it 'logs using turbo mode' do
expect_next_instance_of(LooseForeignKeys::CleanupWorker) do |instance|
expect(instance).to receive(:log_extra_metadata_on_done).with(:stats, a_hash_including(turbo_mode: true))
end
perform_for(db: database_name)
end
end
end
end
end