diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2019-09-11 09:06:33 +0000 |
---|---|---|
committer | Kushal Pandya <kushalspandya@gmail.com> | 2019-09-11 09:06:33 +0000 |
commit | 48b98b5898e15ab4bb1db47e201fef8db68dc34d (patch) | |
tree | 70edcf4953d88d542243fe1d288ff249c02c9d9f | |
parent | bfaa96d586668678893e295062495f2c35b73c2a (diff) | |
download | gitlab-ce-48b98b5898e15ab4bb1db47e201fef8db68dc34d.tar.gz |
Enables Run Pipeline button to be rendered
In the Merge Request view, under pipelines tab
the user can see a run pipeline button
Adds axios post request to button click
Adds the logic to handle the user click,
refresh the table and disable the button while thee
request is being made
Updates UI for desktop and mobile
Adds specs
Regenerates potfile
Follow-up after review
Uses .finally to avoid code repetition
18 files changed, 496 insertions, 43 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1d97ad5ec11..992c5e5e330 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -36,6 +36,7 @@ const Api = { branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', createBranchPath: '/api/:version/projects/:id/repository/branches', releasesPath: '/api/:version/projects/:id/releases', + mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines', adminStatisticsPath: 'api/:version/application/statistics', group(groupId, callback) { @@ -371,6 +372,14 @@ const Api = { }); }, + postMergeRequestPipeline(id, { mergeRequestId }) { + const url = Api.buildUrl(this.mergeRequestsPipeline) + .replace(':id', encodeURIComponent(id)) + .replace(':merge_request_iid', mergeRequestId); + + return axios.post(url); + }, + releases(id) { const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(id)); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 4890f99e9d1..e5b030d4900 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -1,14 +1,19 @@ <script> -import PipelinesService from '../../pipelines/services/pipelines_service'; -import PipelineStore from '../../pipelines/stores/pipelines_store'; -import pipelinesMixin from '../../pipelines/mixins/pipelines'; -import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue'; -import { getParameterByName } from '../../lib/utils/common_utils'; -import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; +import PipelinesService from '~/pipelines/services/pipelines_service'; +import PipelineStore from '~/pipelines/stores/pipelines_store'; +import pipelinesMixin from '~/pipelines/mixins/pipelines'; +import eventHub from '~/pipelines/event_hub'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin'; +import bp from '~/breakpoints'; export default { components: { TablePagination, + GlButton, + GlLoadingIcon, }, mixins: [pipelinesMixin, CIPaginationMixin], props: { @@ -33,6 +38,21 @@ export default { required: false, default: 'child', }, + canRunPipeline: { + type: Boolean, + required: false, + default: false, + }, + projectId: { + type: String, + required: false, + default: '', + }, + mergeRequestId: { + type: Number, + required: false, + default: 0, + }, }, data() { @@ -53,6 +73,41 @@ export default { shouldRenderErrorState() { return this.hasError && !this.isLoading; }, + /** + * The Run Pipeline button can only be rendered when: + * - In MR view - we use `canRunPipeline` for that purpose + * - If the latest pipeline has the `detached_merge_request_pipeline` flag + * + * @returns {Boolean} + */ + canRenderPipelineButton() { + return this.canRunPipeline && this.latestPipelineDetachedFlag; + }, + /** + * Checks if either `detached_merge_request_pipeline` or + * `merge_request_pipeline` are tru in the first + * object in the pipelines array. + * + * @returns {Boolean} + */ + latestPipelineDetachedFlag() { + const latest = this.state.pipelines[0]; + return ( + latest && + latest.flags && + (latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline) + ); + }, + /** + * When we are on Desktop and the button is visible + * we need to add a negative margin to the table + * to make it inline with the button + * + * @returns {Boolean} + */ + shouldAddNegativeMargin() { + return this.canRenderPipelineButton && bp.isDesktop(); + }, }, created() { this.service = new PipelinesService(this.endpoint); @@ -77,6 +132,22 @@ export default { this.$el.parentElement.dispatchEvent(updatePipelinesEvent); } }, + /** + * When the user clicks on the Run Pipeline button + * we need to make a post request and + * to update the table content once the request is finished. + * + * We are emitting an event through the eventHub using the old pattern + * to make use of the code in mixins/pipelines.js that handles all the + * table events + * + */ + onClickRunPipeline() { + eventHub.$emit('runMergeRequestPipeline', { + projectId: this.projectId, + mergeRequestId: this.mergeRequestId, + }); + }, }, }; </script> @@ -99,11 +170,25 @@ export default { /> <div v-else-if="shouldRenderTable" class="table-holder"> + <div v-if="canRenderPipelineButton" class="nav justify-content-end"> + <gl-button + v-if="canRenderPipelineButton" + variant="success" + class="js-run-mr-pipeline prepend-top-10 btn-wide-on-xs" + :disabled="state.isRunningMergeRequestPipeline" + @click="onClickRunPipeline" + > + <gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline /> + {{ s__('Pipelines|Run Pipeline') }} + </gl-button> + </div> + <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" + :class="{ 'negative-margin-top': shouldAddNegativeMargin }" /> </div> diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index b6868e63716..52674107df2 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -333,7 +333,8 @@ export default class MergeRequestTabs { mountPipelinesView() { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - const { CommitPipelinesTable } = gl; + const { CommitPipelinesTable, mrWidgetData } = gl; + this.commitPipelinesTable = new CommitPipelinesTable({ propsData: { endpoint: pipelineTableViewEl.dataset.endpoint, @@ -341,6 +342,9 @@ export default class MergeRequestTabs { emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, + canRunPipeline: true, + projectId: pipelineTableViewEl.dataset.projectId, + mergeRequestId: mrWidgetData ? mrWidgetData.iid : null, }, }).$mount(); diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 126a9a47a2b..876b30299fb 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -1,7 +1,7 @@ import Visibility from 'visibilityjs'; import { GlLoadingIcon } from '@gitlab/ui'; import { __ } from '../../locale'; -import Flash from '../../flash'; +import createFlash from '../../flash'; import Poll from '../../lib/utils/poll'; import EmptyState from '../components/empty_state.vue'; import SvgBlankState from '../components/blank_state.vue'; @@ -62,6 +62,7 @@ export default { eventHub.$on('clickedDropdown', this.updateTable); eventHub.$on('updateTable', this.updateTable); eventHub.$on('refreshPipelinesTable', this.fetchPipelines); + eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); @@ -69,6 +70,7 @@ export default { eventHub.$off('clickedDropdown', this.updateTable); eventHub.$off('updateTable', this.updateTable); eventHub.$off('refreshPipelinesTable', this.fetchPipelines); + eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline); }, destroyed() { this.poll.stop(); @@ -110,7 +112,7 @@ export default { // Stop polling this.poll.stop(); // Restarting the poll also makes an initial request - this.poll.restart(); + return this.poll.restart(); }, fetchPipelines() { if (!this.isMakingRequest) { @@ -156,7 +158,31 @@ export default { this.service .postAction(endpoint) .then(() => this.updateTable()) - .catch(() => Flash(__('An error occurred while making the request.'))); + .catch(() => createFlash(__('An error occurred while making the request.'))); + }, + + /** + * When the user clicks on the run pipeline button + * we toggle the state of the button to be disabled + * + * Once the post request has finished, we fetch the + * pipelines again to show the most recent data + * + * Once the pipeline has been updated, we toggle back the + * loading state and re-enable the run pipeline button + */ + runMergeRequestPipeline(options) { + this.store.toggleIsRunningPipeline(true); + + this.service + .runMRPipeline(options) + .then(() => this.updateTable()) + .catch(() => { + createFlash( + __('An error occurred while trying to run a new pipeline for this Merge Request.'), + ); + }) + .finally(() => this.store.toggleIsRunningPipeline(false)); }, }, }; diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js index 8317d3f4510..3c755db23dc 100644 --- a/app/assets/javascripts/pipelines/services/pipelines_service.js +++ b/app/assets/javascripts/pipelines/services/pipelines_service.js @@ -1,4 +1,5 @@ import axios from '../../lib/utils/axios_utils'; +import Api from '~/api'; export default class PipelinesService { /** @@ -39,4 +40,9 @@ export default class PipelinesService { postAction(endpoint) { return axios.post(`${endpoint}.json`); } + + // eslint-disable-next-line class-methods-use-this + runMRPipeline({ projectId, mergeRequestId }) { + return Api.postMergeRequestPipeline(projectId, { mergeRequestId }); + } } diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index 651251d2623..a4bbada89c8 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -7,6 +7,9 @@ export default class PipelinesStore { this.state.pipelines = []; this.state.count = {}; this.state.pageInfo = {}; + + // Used in MR Pipelines tab + this.state.isRunningMergeRequestPipeline = false; } storePipelines(pipelines = []) { @@ -29,4 +32,13 @@ export default class PipelinesStore { this.state.pageInfo = paginationInfo; } + + /** + * Toggles the isRunningPipeline flag + * + * @param {Boolean} value + */ + toggleIsRunningPipeline(value = false) { + this.state.isRunningMergeRequestPipeline = value; + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 15a779dde1d..faa0a9909d5 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -726,6 +726,7 @@ $pipeline-dropdown-line-height: 20px; $pipeline-dropdown-status-icon-size: 18px; $ci-action-dropdown-button-size: 24px; $ci-action-dropdown-svg-size: 12px; +$pipelines-table-header-height: 40px; /* CI variable lists diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d4bd5b1b7dc..cda6c9ce0cc 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -26,6 +26,10 @@ } .pipelines { + .negative-margin-top { + margin-top: -$pipelines-table-header-height; + } + .stage { max-width: 90px; width: 90px; diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 68b35072f26..81c354f1c8f 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -5,4 +5,5 @@ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), + "project-id": @project.id, } } diff --git a/changelogs/unreleased/65940-run-pipeline.yml b/changelogs/unreleased/65940-run-pipeline.yml new file mode 100644 index 00000000000..c0e89a19373 --- /dev/null +++ b/changelogs/unreleased/65940-run-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Run Pipeline button & API for MR Pipelines +merge_request: 31722 +author: +type: added diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 0d030ef30c8..c7637ad23de 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -821,6 +821,66 @@ Parameters: ] ``` +## Create MR Pipeline + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31722) in Gitlab 12.3. + +Create a new [pipeline for a merge request](../ci/merge_request_pipelines/index.md). A pipeline created via this endpoint will not run a regular branch/tag pipeline, it requires `.gitlab-ci.yml` to be configured with `only: [merge_requests]` to create jobs. + +The new pipeline can be: + +- A detached merge request pipeline. +- A [pipeline for merged results](../ci/merge_request_pipelines/pipelines_for_merged_results/index.md) + if the [project setting is enabled](../ci/merge_request_pipelines/pipelines_for_merged_results/index.md#enabling-pipelines-for-merged-results). + +``` +POST /projects/:id/merge_requests/:merge_request_iid/pipelines +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) +- `merge_request_iid` (required) - The internal ID of the merge request + +```json +{ + "id": 2, + "sha": "b83d6e391c22777fca1ed3012fce84f633d7fed0", + "ref": "refs/merge-requests/1/head", + "status": "pending", + "web_url": "http://localhost/user1/project1/pipelines/2", + "before_sha": "0000000000000000000000000000000000000000", + "tag": false, + "yaml_errors": null, + "user": { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://example.com" + }, + "created_at": "2019-09-04T19:20:18.267Z", + "updated_at": "2019-09-04T19:20:18.459Z", + "started_at": null, + "finished_at": null, + "committed_at": null, + "duration": null, + "coverage": null, + "detailed_status": { + "icon": "status_pending", + "text": "pending", + "label": "pending", + "group": "pending", + "tooltip": "pending", + "has_details": false, + "details_path": "/user1/project1/pipelines/2", + "illustration": null, + "favicon": "/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png" + } +} +``` + ## Create MR Creates a new merge request. diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 64ee82cd775..4c092f10729 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -317,6 +317,26 @@ module API present paginate(pipelines), with: Entities::PipelineBasic end + desc 'Create a pipeline for merge request' do + success Entities::Pipeline + end + post ':id/merge_requests/:merge_request_iid/pipelines' do + authorize! :create_pipeline, user_project + + pipeline = ::MergeRequests::CreatePipelineService + .new(user_project, current_user, allow_duplicate: true) + .execute(find_merge_request_with_access(params[:merge_request_iid])) + + if pipeline.nil? + not_allowed! + elsif pipeline.persisted? + status :ok + present pipeline, with: Entities::Pipeline + else + render_validation_error!(pipeline) + end + end + desc 'Update a merge request' do success Entities::MergeRequest end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 32deab7dd68..f2d3a39d593 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1146,6 +1146,9 @@ msgstr "" msgid "An error occurred while triggering the job." msgstr "" +msgid "An error occurred while trying to run a new pipeline for this Merge Request." +msgstr "" + msgid "An error occurred while validating username" msgstr "" diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb index f04317a59ee..7a8b938486a 100644 --- a/spec/features/merge_request/user_sees_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_pipelines_spec.rb @@ -45,6 +45,38 @@ describe 'Merge request > User sees pipelines', :js do expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.") 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('.js-run-mr-pipeline')).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('.js-run-mr-pipeline')).to have_text('Run Pipeline') + end + end end context 'without pipelines' do diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index fec01b1f0a3..46aca2b7f03 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; +import Api from '~/api'; import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -10,6 +11,13 @@ describe('Pipelines table in Commits and Merge requests', function() { let PipelinesTable; let mock; let vm; + const props = { + endpoint: 'endpoint.json', + helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + errorStateSvgPath: 'foo', + autoDevopsHelpPath: 'foo', + }; preloadFixtures(jsonFixtureName); @@ -32,13 +40,7 @@ describe('Pipelines table in Commits and Merge requests', function() { beforeEach(function() { mock.onGet('endpoint.json').reply(200, []); - vm = mountComponent(PipelinesTable, { - endpoint: 'endpoint.json', - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - errorStateSvgPath: 'foo', - autoDevopsHelpPath: 'foo', - }); + vm = mountComponent(PipelinesTable, props); }); it('should render the empty state', function(done) { @@ -54,13 +56,7 @@ describe('Pipelines table in Commits and Merge requests', function() { describe('with pipelines', () => { beforeEach(() => { mock.onGet('endpoint.json').reply(200, [pipeline]); - vm = mountComponent(PipelinesTable, { - endpoint: 'endpoint.json', - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - errorStateSvgPath: 'foo', - autoDevopsHelpPath: 'foo', - }); + vm = mountComponent(PipelinesTable, props); }); it('should render a table with the received pipelines', done => { @@ -111,30 +107,145 @@ describe('Pipelines table in Commits and Merge requests', function() { done(); }); - vm = mountComponent(PipelinesTable, { - endpoint: 'endpoint.json', - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - errorStateSvgPath: 'foo', - autoDevopsHelpPath: 'foo', - }); + vm = mountComponent(PipelinesTable, props); element.appendChild(vm.$el); }); }); }); + describe('run pipeline button', () => { + let pipelineCopy; + + beforeEach(() => { + pipelineCopy = Object.assign({}, pipeline); + }); + + describe('when latest pipeline has detached flag and canRunPipeline is true', () => { + it('renders the run pipeline button', done => { + pipelineCopy.flags.detached_merge_request_pipeline = true; + pipelineCopy.flags.merge_request_pipeline = true; + + mock.onGet('endpoint.json').reply(200, [pipelineCopy]); + + vm = mountComponent( + PipelinesTable, + Object.assign({}, props, { + canRunPipeline: true, + }), + ); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-run-mr-pipeline')).not.toBeNull(); + done(); + }); + }); + }); + + describe('when latest pipeline has detached flag and canRunPipeline is false', () => { + it('does not render the run pipeline button', done => { + pipelineCopy.flags.detached_merge_request_pipeline = true; + pipelineCopy.flags.merge_request_pipeline = true; + + mock.onGet('endpoint.json').reply(200, [pipelineCopy]); + + vm = mountComponent( + PipelinesTable, + Object.assign({}, props, { + canRunPipeline: false, + }), + ); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull(); + done(); + }); + }); + }); + + describe('when latest pipeline does not have detached flag and canRunPipeline is true', () => { + it('does not render the run pipeline button', done => { + pipelineCopy.flags.detached_merge_request_pipeline = false; + pipelineCopy.flags.merge_request_pipeline = false; + + mock.onGet('endpoint.json').reply(200, [pipelineCopy]); + + vm = mountComponent( + PipelinesTable, + Object.assign({}, props, { + canRunPipeline: true, + }), + ); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull(); + done(); + }); + }); + }); + + describe('when latest pipeline does not have detached flag and merge_request_pipeline is true', () => { + it('does not render the run pipeline button', done => { + pipelineCopy.flags.detached_merge_request_pipeline = false; + pipelineCopy.flags.merge_request_pipeline = true; + + mock.onGet('endpoint.json').reply(200, [pipelineCopy]); + + vm = mountComponent( + PipelinesTable, + Object.assign({}, props, { + canRunPipeline: false, + }), + ); + + setTimeout(() => { + expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull(); + done(); + }); + }); + }); + + describe('on click', () => { + beforeEach(() => { + pipelineCopy.flags.detached_merge_request_pipeline = true; + + mock.onGet('endpoint.json').reply(200, [pipelineCopy]); + + vm = mountComponent( + PipelinesTable, + Object.assign({}, props, { + canRunPipeline: true, + projectId: '5', + mergeRequestId: 3, + }), + ); + }); + + it('updates the loading state', done => { + spyOn(Api, 'postMergeRequestPipeline').and.returnValue(Promise.resolve()); + + setTimeout(() => { + vm.$el.querySelector('.js-run-mr-pipeline').click(); + + vm.$nextTick(() => { + expect(vm.state.isRunningMergeRequestPipeline).toBe(true); + + setTimeout(() => { + expect(vm.state.isRunningMergeRequestPipeline).toBe(false); + + done(); + }); + }); + }); + }); + }); + }); + describe('unsuccessfull request', () => { beforeEach(() => { mock.onGet('endpoint.json').reply(500, []); - vm = mountComponent(PipelinesTable, { - endpoint: 'endpoint.json', - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - errorStateSvgPath: 'foo', - autoDevopsHelpPath: 'foo', - }); + vm = mountComponent(PipelinesTable, props); }); it('should render error state', function(done) { diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 146e479adef..d5ad70194cb 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -579,14 +579,22 @@ describe Ci::Pipeline, :mailer do end describe 'Validations for merge request pipelines' do - let(:pipeline) { build(:ci_pipeline, source: source, merge_request: merge_request) } + let(:pipeline) do + build(:ci_pipeline, source: source, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end context 'when source is merge request' do let(:source) { :merge_request_event } context 'when merge request is specified' do - let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') } - it { expect(pipeline).to be_valid } end @@ -601,8 +609,6 @@ describe Ci::Pipeline, :mailer do let(:source) { :web } context 'when merge request is specified' do - let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') } - it { expect(pipeline).not_to be_valid } end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 15d6db42760..8179da2f97c 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1033,6 +1033,70 @@ describe API::MergeRequests do end end + describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do + before do + allow_any_instance_of(Ci::Pipeline) + .to receive(:ci_yaml_file) + .and_return(YAML.dump({ + rspec: { + script: 'ls', + only: ['merge_requests'] + } + })) + end + + let(:project) do + create(:project, :private, :repository, + creator: user, + namespace: user.namespace, + only_allow_merge_if_pipeline_succeeds: false) + end + + let(:merge_request) do + create(:merge_request, :with_detached_merge_request_pipeline, + milestone: milestone1, + author: user, + assignees: [user], + source_project: project, + target_project: project, + title: 'Test', + created_at: base_time) + end + + let(:merge_request_iid) { merge_request.iid } + let(:authenticated_user) { user } + + let(:request) do + post api("/projects/#{project.id}/merge_requests/#{merge_request_iid}/pipelines", authenticated_user) + end + + context 'when authorized' do + it 'creates and returns the new Pipeline' do + expect { request }.to change(Ci::Pipeline, :count).by(1) + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_a Hash + end + end + + context 'when unauthorized' do + let(:authenticated_user) { create(:user) } + + it 'responds with a blank 404' do + expect { request }.not_to change(Ci::Pipeline, :count) + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when the merge request does not exist' do + let(:merge_request_iid) { 777 } + + it 'responds with a blank 404' do + expect { request }.not_to change(Ci::Pipeline, :count) + expect(response).to have_gitlab_http_status(404) + end + end + end + describe 'POST /projects/:id/merge_requests' do context 'support for deprecated assignee_id' do let(:params) do diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb index 9479439bde8..576e8498e4d 100644 --- a/spec/services/merge_requests/create_pipeline_service_spec.rb +++ b/spec/services/merge_requests/create_pipeline_service_spec.rb @@ -38,6 +38,10 @@ describe MergeRequests::CreatePipelineService do expect(subject).to be_detached_merge_request_pipeline end + it 'defaults to merge_request_event' do + expect(subject.source).to eq('merge_request_event') + end + context 'when service is called multiple times' do it 'creates a pipeline once' do expect do |