diff options
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 |