summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKushal Pandya <kushalspandya@gmail.com>2019-09-11 09:06:34 +0000
committerKushal Pandya <kushalspandya@gmail.com>2019-09-11 09:06:34 +0000
commitaf03cb0683b621433f268392be4a5e0261a8c7e5 (patch)
tree70edcf4953d88d542243fe1d288ff249c02c9d9f
parentbfaa96d586668678893e295062495f2c35b73c2a (diff)
parent48b98b5898e15ab4bb1db47e201fef8db68dc34d (diff)
downloadgitlab-ce-af03cb0683b621433f268392be4a5e0261a8c7e5.tar.gz
Merge branch '65940-run-pipeline' into 'master'
Resolve "Run pipeline button for "Pipelines for merge requests"" Closes #65940 See merge request gitlab-org/gitlab-ce!31722
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue97
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js32
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/pipelines_store.js12
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--changelogs/unreleased/65940-run-pipeline.yml5
-rw-r--r--doc/api/merge_requests.md60
-rw-r--r--lib/api/merge_requests.rb20
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb32
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js167
-rw-r--r--spec/models/ci/pipeline_spec.rb16
-rw-r--r--spec/requests/api/merge_requests_spec.rb64
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb4
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