post merge pipeline and environments status

This commit is contained in:
Alessio Caiazza 2018-10-29 11:34:41 +00:00 committed by Nick Thomas
parent 289651e26f
commit dadc046d3a
22 changed files with 589 additions and 119 deletions

View file

@ -2,6 +2,7 @@
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
import { __ } from '~/locale';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@ -31,6 +32,11 @@ export default {
required: true,
},
},
deployedTextMap: {
running: __('Deploying to'),
success: __('Deployed to'),
failed: __('Failed to deploy to'),
},
data() {
const features = window.gon.features || {};
return {
@ -54,10 +60,13 @@ export default {
hasMetrics() {
return !!this.deployment.metrics_url;
},
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
},
},
methods: {
stopEnvironment() {
const msg = 'Are you sure you want to stop this environment?';
const msg = __('Are you sure you want to stop this environment?');
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
@ -87,10 +96,10 @@ export default {
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
<div class="deployment-info">
<div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta">
<span>
Deployed to
{{ deployedText }}
</span>
<tooltip-on-truncate
:title="deployment.name"

View file

@ -1,4 +1,6 @@
<script>
import _ from 'underscore';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
@ -80,6 +82,7 @@ export default {
const service = this.createService(store);
return {
mr: store,
state: store.state,
service,
};
},
@ -103,6 +106,17 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState)
);
},
shouldRenderMergedPipeline() {
return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline);
},
},
watch: {
state(newVal, oldVal) {
if (newVal !== oldVal && this.shouldRenderMergedPipeline) {
// init polling
this.initPostMergeDeploymentsPolling();
}
}
},
created() {
this.initPolling();
@ -112,11 +126,19 @@ export default {
mounted() {
this.setFaviconHelper();
this.initDeploymentsPolling();
if (this.shouldRenderMergedPipeline) {
this.initPostMergeDeploymentsPolling();
}
},
beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus);
this.pollingInterval.destroy();
this.deploymentsInterval.destroy();
if (this.postMergeDeploymentsInterval) {
this.postMergeDeploymentsInterval.destroy();
}
},
methods: {
createService(store) {
@ -146,7 +168,13 @@ export default {
cb.call(null, data);
}
})
.catch(() => createFlash('Something went wrong. Please try again.'));
.catch(() => createFlash(__('Something went wrong. Please try again.')));
},
setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) {
return setFaviconOverlay(this.mr.ciStatusFaviconPath);
}
return Promise.resolve();
},
initPolling() {
this.pollingInterval = new SmartInterval({
@ -158,8 +186,14 @@ export default {
});
},
initDeploymentsPolling() {
this.deploymentsInterval = new SmartInterval({
callback: this.fetchDeployments,
this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments);
},
initPostMergeDeploymentsPolling() {
this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments);
},
deploymentsPoll(callback) {
return new SmartInterval({
callback,
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
@ -167,26 +201,29 @@ export default {
immediateExecution: true,
});
},
setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) {
return setFaviconOverlay(this.mr.ciStatusFaviconPath);
}
return Promise.resolve();
fetchDeployments(target) {
return this.service.fetchDeployments(target);
},
fetchDeployments() {
return this.service
.fetchDeployments()
.then(res => res.data)
.then(data => {
fetchPreMergeDeployments() {
return this.fetchDeployments()
.then(({ data }) => {
if (data.length) {
this.mr.deployments = data;
}
})
.catch(() => {
createFlash(
'Something went wrong while fetching the environments for this merge request. Please try again.',
);
});
.catch(() => this.throwDeploymentsError());
},
fetchPostMergeDeployments(){
return this.fetchDeployments('merge_commit')
.then(({ data }) => {
if (data.length) {
this.mr.postMergeDeployments = data;
}
})
.catch(() => this.throwDeploymentsError());
},
throwDeploymentsError() {
createFlash(__('Something went wrong while fetching the environments for this merge request. Please try again.'));
},
fetchActionsContent() {
this.service
@ -199,7 +236,7 @@ export default {
Project.initRefSwitcher();
}
})
.catch(() => createFlash('Something went wrong. Please try again.'));
.catch(() => createFlash(__('Something went wrong. Please try again.')));
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
@ -267,7 +304,8 @@ export default {
/>
<deployment
v-for="deployment in mr.deployments"
:key="deployment.id"
:key="`pre-merge-deploy-${deployment.id}`"
class="js-pre-merge-deploy"
:deployment="deployment"
/>
<div class="mr-section-container">
@ -308,5 +346,22 @@ export default {
<mr-widget-merge-help />
</div>
</div>
<template v-if="shouldRenderMergedPipeline">
<mr-widget-pipeline
class="js-post-merge-pipeline prepend-top-default"
:pipeline="mr.mergePipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:source-branch="mr.targetBranch"
:source-branch-link="mr.targetBranch"
/>
<deployment
v-for="postMergeDeployment in mr.postMergeDeployments"
:key="`post-merge-deploy-${postMergeDeployment.id}`"
:deployment="postMergeDeployment"
class="js-post-deployment"
/>
</template>
</div>
</template>

View file

@ -21,8 +21,12 @@ export default class MRWidgetService {
return axios.delete(this.endpoints.sourceBranchPath);
}
fetchDeployments() {
return axios.get(this.endpoints.ciEnvironmentsStatusPath);
fetchDeployments(targetParam) {
return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
params: {
environment_target: targetParam
}
});
}
poll() {

View file

@ -32,7 +32,9 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
this.initRebase(data);
if (data.issues_links) {

View file

@ -201,9 +201,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def ci_environments_status
environments = @merge_request.environments_for(current_user).map do |environment|
EnvironmentStatus.new(environment, @merge_request)
end
environments = if ci_environments_status_on_merge_result?
EnvironmentStatus.after_merge_request(@merge_request, current_user)
else
EnvironmentStatus.for_merge_request(@merge_request, current_user)
end
render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments)
end
@ -241,6 +243,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
def ci_environments_status_on_merge_result?
params[:environment_target] == 'merge_commit'
end
def target_branch_missing?
@merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end

View file

@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base
metrics&.merge(deployment_time: created_at.to_i) || {}
end
def status
'success'
end
private
def prometheus_adapter

View file

@ -3,21 +3,33 @@
class EnvironmentStatus
include Gitlab::Utils::StrongMemoize
attr_reader :environment, :merge_request
attr_reader :environment, :merge_request, :sha
delegate :id, to: :environment
delegate :name, to: :environment
delegate :project, to: :environment
delegate :deployed_at, to: :deployment, allow_nil: true
delegate :status, to: :deployment
def initialize(environment, merge_request)
def self.for_merge_request(mr, user)
build_environments_status(mr, user, mr.head_pipeline)
end
def self.after_merge_request(mr, user)
return [] unless mr.merged?
build_environments_status(mr, user, mr.merge_pipeline)
end
def initialize(environment, merge_request, sha)
@environment = environment
@merge_request = merge_request
@sha = sha
end
def deployment
strong_memoize(:deployment) do
environment.first_deployment_for(merge_request.diff_head_sha)
environment.first_deployment_for(sha)
end
end
@ -26,10 +38,9 @@ class EnvironmentStatus
end
def changes
sha = merge_request.diff_head_sha
return [] if project.route_map_for(sha).nil?
changed_files.map { |file| build_change(file, sha) }.compact
changed_files.map { |file| build_change(file) }.compact
end
def changed_files
@ -41,7 +52,7 @@ class EnvironmentStatus
PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze
def build_change(file, sha)
def build_change(file)
public_path = project.public_path_for_source_path(file.new_path, sha)
return if public_path.nil?
@ -53,4 +64,22 @@ class EnvironmentStatus
external_url: environment.external_url_for(file.new_path, sha)
}
end
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline.present?
find_environments(user, pipeline).map do |environment|
EnvironmentStatus.new(environment, mr, pipeline.sha)
end
end
private_class_method :build_environments_status
def self.find_environments(user, pipeline)
env_ids = Deployment.where(deployable: pipeline.builds).select(:environment_id)
Environment.available.where(id: env_ids).select do |environment|
Ability.allowed?(user, :read_environment, environment)
end
end
private_class_method :find_environments
end

View file

@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end
def merge_pipeline
return unless merged?
target_project.pipeline_for(target_branch, merge_commit_sha)
end
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.

View file

@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity
expose :id
expose :name
expose :status
expose :url do |es|
project_environment_path(es.project, es.environment)

View file

@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_commit_message
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
# Booleans
expose :merge_ongoing?, as: :merge_ongoing

View file

@ -0,0 +1,5 @@
---
title: Show post-merge pipeline in merge request page
merge_request: 22292
author:
type: added

View file

@ -648,6 +648,9 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?"
msgstr ""
msgid "Are you sure you want to stop this environment?"
msgstr ""
msgid "Are you sure?"
msgstr ""
@ -2324,6 +2327,12 @@ msgstr ""
msgid "DeployTokens|Your new project deploy token has been created."
msgstr ""
msgid "Deployed to"
msgstr ""
msgid "Deploying to"
msgstr ""
msgid "Deprioritize label"
msgstr ""
@ -2750,6 +2759,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
msgid "Failed to deploy to"
msgstr ""
msgid "Failed to load emoji list."
msgstr ""
@ -5604,6 +5616,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""
msgid "Something went wrong while fetching the projects."
msgstr ""

View file

@ -749,13 +749,15 @@ describe Projects::MergeRequestsController do
describe 'GET ci_environments_status' do
context 'the environment is from a forked project' do
let!(:forked) { fork_project(project, user, repository: true) }
let!(:environment) { create(:environment, project: forked) }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
let(:admin) { create(:admin) }
let(:forked) { fork_project(project, user, repository: true) }
let(:sha) { forked.commit.sha }
let(:environment) { create(:environment, project: forked) }
let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) }
let(:merge_request) do
create(:merge_request, source_project: forked, target_project: project)
create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline)
end
it 'links to the environment on that project' do
@ -764,6 +766,35 @@ describe Projects::MergeRequestsController do
expect(json_response.first['url']).to match /#{forked.full_path}/
end
context "when environment_target is 'merge_commit'" do
it 'returns nothing' do
get_ci_environments_status(environment_target: 'merge_commit')
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
context 'when is merged' do
let(:source_environment) { create(:environment, project: project) }
let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") }
let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) }
let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) }
let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) }
before do
merge_request.update!(merge_commit_sha: merge_commit_sha)
merge_request.mark_as_merged!
end
it 'returns the enviroment on the source project' do
get_ci_environments_status(environment_target: 'merge_commit')
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first['url']).to match /#{project.full_path}/
end
end
end
# we're trying to reduce the overall number of queries for this method.
# set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287
it 'keeps queries in check' do
@ -772,11 +803,15 @@ describe Projects::MergeRequestsController do
expect(control_count).to be <= 137
end
def get_ci_environments_status
get :ci_environments_status,
def get_ci_environments_status(extra_params = {})
params = {
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
id: merge_request.iid, format: 'json'
id: merge_request.iid,
format: 'json'
}
get :ci_environments_status, params.merge(extra_params)
end
end
end

View file

@ -3,15 +3,19 @@ require 'rails_helper'
describe 'Merge request > User sees deployment widget', :js do
describe 'when deployed to an environment' do
let(:user) { create(:user) }
let(:project) { merge_request.target_project }
let(:merge_request) { create(:merge_request, :merged) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:environment) { create(:environment, project: project) }
let(:role) { :developer }
let(:sha) { project.commit('master').id }
let!(:deployment) { create(:deployment, environment: environment, sha: sha) }
let(:ref) { merge_request.target_branch }
let(:sha) { project.commit(ref).id }
let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) }
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) }
let!(:manual) { }
before do
merge_request.update!(merge_commit_sha: sha)
project.add_user(user, role)
sign_in(user)
visit project_merge_request_path(project, merge_request)
@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do
end
context 'with stop action' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
let(:deployment) do
create(:deployment, environment: environment, ref: merge_request.target_branch,
sha: sha, deployable: build, on_stop: 'close_app')
end
before do
deployment.update!(on_stop: manual.name)
wait_for_requests
end

View file

@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request' do
let!(:environment) { create(:environment, project: project) }
let(:sha) { project.commit(merge_request.source_branch).sha }
let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) do
create(:deployment, environment: environment,
ref: 'feature',
sha: merge_request.diff_head_sha)
ref: merge_request.source_branch,
deployable: build,
sha: sha)
end
before do
merge_request.update!(head_pipeline: pipeline)
visit project_merge_request_path(project, merge_request)
end
it 'shows environments link' do
wait_for_requests
page.within('.mr-widget-heading') do
page.within('.js-pre-merge-deploy') do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end

View file

@ -46,6 +46,7 @@
"diff_head_commit_short_id": { "type": ["string", "null"] },
"merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] },
"merge_pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" },
"source_branch_exists": { "type": "boolean" },
"mergeable_discussions_state": { "type": "boolean" },

View file

@ -2,54 +2,48 @@ import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility';
const deploymentMockData = {
id: 15,
name: 'review/diplo',
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
external_url: 'http://diplo.',
external_url_formatted: 'diplo.',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes: [
{
path: 'index.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
},
{
path: 'imgs/gallery.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
},
{
path: 'about/',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
},
],
};
const createComponent = () => {
const Component = Vue.extend(deploymentComponent);
return new Component({
el: document.createElement('div'),
propsData: { deployment: { ...deploymentMockData } },
});
};
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Deployment component', () => {
let vm;
const Component = Vue.extend(deploymentComponent);
const deploymentMockData = {
id: 15,
name: 'review/diplo',
url: '/root/review-apps/environments/15',
stop_url: '/root/review-apps/environments/15/stop',
metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
external_url: 'http://gitlab.com.',
external_url_formatted: 'gitlab',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes: [
{
path: 'index.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
},
{
path: 'imgs/gallery.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
},
{
path: 'about/',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
},
],
};
beforeEach(() => {
vm = createComponent();
});
let vm;
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('', () => {
beforeEach(() => {
vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
describe('deployTimeago', () => {
it('return formatted date', () => {
const readable = getTimeago().format(deploymentMockData.deployed_at);
@ -111,9 +105,7 @@ describe('Deployment component', () => {
expect(vm.hasDeploymentMeta).toEqual(false);
});
});
});
describe('methods', () => {
describe('stopEnvironment', () => {
const url = '/foo/bar';
const returnPromise = () =>
@ -152,42 +144,33 @@ describe('Deployment component', () => {
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
});
});
});
describe('template', () => {
let el;
beforeEach(() => {
vm = createComponent(deploymentMockData);
el = vm.$el;
});
it('renders deployment name', () => {
expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
expect(vm.$el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(
deploymentMockData.url,
);
expect(el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
expect(vm.$el.querySelector('.js-deploy-meta').innerText).toContain(deploymentMockData.name);
});
it('renders external URL', () => {
expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
expect(vm.$el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(
deploymentMockData.external_url,
);
expect(el.querySelector('.js-deploy-url').innerText).toContain('View app');
expect(vm.$el.querySelector('.js-deploy-url').innerText).toContain('View app');
});
it('renders stop button', () => {
expect(el.querySelector('.btn')).not.toBeNull();
expect(vm.$el.querySelector('.btn')).not.toBeNull();
});
it('renders deployment time', () => {
expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
expect(vm.$el.querySelector('.js-deploy-time').innerText).toContain(vm.deployTimeago);
});
it('renders metrics component', () => {
expect(el.querySelector('.js-mr-memory-usage')).not.toBeNull();
expect(vm.$el.querySelector('.js-mr-memory-usage')).not.toBeNull();
});
});
@ -196,8 +179,7 @@ describe('Deployment component', () => {
window.gon = window.gon || {};
window.gon.features = window.gon.features || {};
window.gon.features.ciEnvironmentsStatusChanges = true;
vm = createComponent(deploymentMockData);
vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
afterEach(() => {
@ -216,7 +198,7 @@ describe('Deployment component', () => {
window.gon.features = window.gon.features || {};
window.gon.features.ciEnvironmentsStatusChanges = false;
vm = createComponent(deploymentMockData);
vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
afterEach(() => {
@ -228,4 +210,44 @@ describe('Deployment component', () => {
expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
});
});
describe('deployment status', () => {
describe('running', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'running' }),
});
});
it('renders information about running deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to');
});
});
describe('success', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'success' }),
});
});
it('renders information about finished deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deployed to');
});
});
describe('failed', () => {
beforeEach(() => {
vm = mountComponent(Component, {
deployment: Object.assign({}, deploymentMockData, { status: 'failed' }),
});
});
it('renders information about finished deployment', () => {
expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain(
'Failed to deploy to',
);
});
});
});
});

View file

@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => {
it('should fetch deployments', done => {
spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }]));
vm.fetchDeployments();
vm.fetchPreMergeDeployments();
setTimeout(() => {
expect(vm.service.fetchDeployments).toHaveBeenCalled();
@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes,
status: 'success'
};
beforeEach(done => {
@ -486,4 +487,189 @@ describe('mrWidgetOptions', () => {
).toEqual(changes.length);
});
});
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
beforeEach(done => {
vm.mr.state = 'merged';
vm.mr.mergePipeline = {
id: 127,
user: {
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: null,
web_url: 'http://localhost:3000/root',
status_tooltip_html: null,
path: '/root',
},
active: true,
coverage: null,
source: 'push',
created_at: '2018-10-22T11:41:35.186Z',
updated_at: '2018-10-22T11:41:35.433Z',
path: '/root/ci-web-terminal/pipelines/127',
flags: {
latest: true,
stuck: true,
auto_devops: false,
yaml_errors: false,
retryable: false,
cancelable: true,
failure_reason: false,
},
details: {
status: {
icon: 'status_pending',
text: 'pending',
label: 'pending',
group: 'pending',
tooltip: 'pending',
has_details: true,
details_path: '/root/ci-web-terminal/pipelines/127',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
},
duration: null,
finished_at: null,
stages: [
{
name: 'test',
title: 'test: pending',
status: {
icon: 'status_pending',
text: 'pending',
label: 'pending',
group: 'pending',
tooltip: 'pending',
has_details: true,
details_path: '/root/ci-web-terminal/pipelines/127#test',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
},
path: '/root/ci-web-terminal/pipelines/127#test',
dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test',
},
],
artifacts: [],
manual_actions: [],
scheduled_actions: [],
},
ref: {
name: 'master',
path: '/root/ci-web-terminal/commits/master',
tag: false,
branch: true,
},
commit: {
id: 'aa1939133d373c94879becb79d91828a892ee319',
short_id: 'aa193913',
title: "Merge branch 'master-test' into 'master'",
created_at: '2018-10-22T11:41:33.000Z',
parent_ids: [
'4622f4dd792468993003caf2e3be978798cbe096',
'76598df914cdfe87132d0c3c40f80db9fa9396a4',
],
message:
"Merge branch 'master-test' into 'master'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
author_name: 'Administrator',
author_email: 'admin@example.com',
authored_date: '2018-10-22T11:41:33.000Z',
committer_name: 'Administrator',
committer_email: 'admin@example.com',
committed_date: '2018-10-22T11:41:33.000Z',
author: {
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: null,
web_url: 'http://localhost:3000/root',
status_tooltip_html: null,
path: '/root',
},
author_gravatar_url: null,
commit_url:
'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
},
cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
};
vm.$nextTick(done);
});
it('renders pipeline block', () => {
expect(vm.$el.querySelector('.js-post-merge-pipeline')).not.toBeNull();
});
describe('with post merge deployments', () => {
beforeEach(done => {
vm.mr.postMergeDeployments = [{
id: 15,
name: 'review/diplo',
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
external_url: 'http://diplo.',
external_url_formatted: 'diplo.',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes: [
{
path: 'index.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
},
{
path: 'imgs/gallery.html',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
},
{
path: 'about/',
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
},
],
status: 'success'
}];
vm.$nextTick(done);
});
it('renders post deployment information', () => {
expect(vm.$el.querySelector('.js-post-deployment')).not.toBeNull();
});
});
});
describe('without information for target branch pipeline', () => {
beforeEach(done => {
vm.mr.state = 'merged';
vm.$nextTick(done);
});
it('does not render pipeline block', () => {
expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
});
});
describe('when state is not merged', () => {
beforeEach(done => {
vm.mr.state = 'archived';
vm.$nextTick(done);
});
it('does not render pipeline block', () => {
expect(vm.$el.querySelector('.js-post-merge-pipeline')).toBeNull();
});
it('does not render post deployment information', () => {
expect(vm.$el.querySelector('.js-post-deployment')).toBeNull();
});
});
});
});

View file

@ -5,13 +5,15 @@ describe EnvironmentStatus do
let(:environment) { deployment.environment}
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
let(:sha) { deployment.sha }
subject(:environment_status) { described_class.new(environment, merge_request) }
subject(:environment_status) { described_class.new(environment, merge_request, sha) }
it { is_expected.to delegate_method(:id).to(:environment) }
it { is_expected.to delegate_method(:name).to(:environment) }
it { is_expected.to delegate_method(:project).to(:environment) }
it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) }
it { is_expected.to delegate_method(:status).to(:deployment) }
describe '#project' do
subject { environment_status.project }
@ -58,4 +60,32 @@ describe EnvironmentStatus do
)
end
end
describe '.for_merge_request' do
let(:admin) { create(:admin) }
let(:pipeline) { create(:ci_pipeline, sha: sha) }
it 'is based on merge_request.head_pipeline' do
expect(merge_request).to receive(:head_pipeline).and_return(pipeline)
expect(merge_request).not_to receive(:merge_pipeline)
described_class.for_merge_request(merge_request, admin)
end
end
describe '.after_merge_request' do
let(:admin) { create(:admin) }
let(:pipeline) { create(:ci_pipeline, sha: sha) }
before do
merge_request.mark_as_merged!
end
it 'is based on merge_request.merge_pipeline' do
expect(merge_request).to receive(:merge_pipeline).and_return(pipeline)
expect(merge_request).not_to receive(:head_pipeline)
described_class.after_merge_request(merge_request, admin)
end
end
end

View file

@ -1058,6 +1058,26 @@ describe MergeRequest do
end
end
describe '#merge_pipeline' do
it 'returns nil when not merged' do
expect(subject.merge_pipeline).to be_nil
end
context 'when the MR is merged' do
let(:sha) { subject.target_project.commit.id }
let(:pipeline) { create(:ci_empty_pipeline, sha: sha, ref: subject.target_branch, project: subject.target_project) }
before do
subject.mark_as_merged!
subject.update_attribute(:merge_commit_sha, pipeline.sha)
end
it 'returns the post-merge pipeline' do
expect(subject.merge_pipeline).to eq(pipeline)
end
end
end
describe '#has_ci?' do
let(:merge_request) { build_stubbed(:merge_request) }

View file

@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do
let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) }
let(:environment_status) { EnvironmentStatus.new(environment, merge_request) }
let(:environment_status) { EnvironmentStatus.new(environment, merge_request, merge_request.diff_head_sha) }
let(:entity) { described_class.new(environment_status, request: request) }
subject { entity.as_json }
@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do
it { is_expected.to include(:deployed_at) }
it { is_expected.to include(:deployed_at_formatted) }
it { is_expected.to include(:changes) }
it { is_expected.to include(:status) }
it { is_expected.not_to include(:stop_url) }
it { is_expected.not_to include(:metrics_url) }

View file

@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do
end
end
describe 'merge_pipeline' do
it 'returns nil' do
expect(subject[:merge_pipeline]).to be_nil
end
context 'when is merged' do
let(:resource) { create(:merged_merge_request, source_project: project, merge_commit_sha: project.commit.id) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.target_branch, sha: resource.merge_commit_sha) }
before do
project.add_maintainer(user)
end
it 'returns merge_pipeline' do
pipeline.reload
pipeline_payload = PipelineDetailsEntity
.represent(pipeline, request: request)
.as_json
expect(subject[:merge_pipeline]).to eq(pipeline_payload)
end
context 'when user cannot read pipelines on target project' do
before do
project.add_guest(user)
end
it 'returns nil' do
expect(subject[:merge_pipeline]).to be_nil
end
end
end
end
describe 'metrics' do
context 'when metrics record exists with merged data' do
before do