Commit dadc046d authored by Alessio Caiazza's avatar Alessio Caiazza Committed by Nick Thomas

post merge pipeline and environments status

parent 289651e2
......@@ -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"
......
<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>
......@@ -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() {
......
......@@ -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) {
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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.
......
......@@ -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)
......
......@@ -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
......
---
title: Show post-merge pipeline in merge request page
merge_request: 22292
author:
type: added
......@@ -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 ""
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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" },
......
......@@ -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';
import mountComponent from '../../helpers/vue_mount_component_helper';
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 = () => {
describe('Deployment component', () => {
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/',
},
],
};
return new Component({
el: document.createElement('div'),
propsData: { deployment: { ...deploymentMockData } },
});
};
describe('Deployment component', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
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);
});