Commit 5a03ee76 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'ac-post-merge-pipeline' into 'master'

post merge pipeline and environments status

Closes #47799

See merge request gitlab-org/gitlab-ce!22292
parents 289651e2 dadc046d
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue'; import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
import { __ } from '~/locale';
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue'; import LoadingButton from '../../vue_shared/components/loading_button.vue';
...@@ -31,6 +32,11 @@ export default { ...@@ -31,6 +32,11 @@ export default {
required: true, required: true,
}, },
}, },
deployedTextMap: {
running: __('Deploying to'),
success: __('Deployed to'),
failed: __('Failed to deploy to'),
},
data() { data() {
const features = window.gon.features || {}; const features = window.gon.features || {};
return { return {
...@@ -54,10 +60,13 @@ export default { ...@@ -54,10 +60,13 @@ export default {
hasMetrics() { hasMetrics() {
return !!this.deployment.metrics_url; return !!this.deployment.metrics_url;
}, },
deployedText() {
return this.$options.deployedTextMap[this.deployment.status];
},
}, },
methods: { methods: {
stopEnvironment() { 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 const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) { if (isConfirmed) {
...@@ -87,10 +96,10 @@ export default { ...@@ -87,10 +96,10 @@ export default {
<div class="ci-widget media"> <div class="ci-widget media">
<div class="media-body"> <div class="media-body">
<div class="deploy-body"> <div class="deploy-body">
<div class="deployment-info"> <div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta"> <template v-if="hasDeploymentMeta">
<span> <span>
Deployed to {{ deployedText }}
</span> </span>
<tooltip-on-truncate <tooltip-on-truncate
:title="deployment.name" :title="deployment.name"
......
<script> <script>
import _ from 'underscore';
import { __ } from '~/locale';
import Project from '~/pages/projects/project'; import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval'; import SmartInterval from '~/smart_interval';
import createFlash from '../flash'; import createFlash from '../flash';
...@@ -80,6 +82,7 @@ export default { ...@@ -80,6 +82,7 @@ export default {
const service = this.createService(store); const service = this.createService(store);
return { return {
mr: store, mr: store,
state: store.state,
service, service,
}; };
}, },
...@@ -103,6 +106,17 @@ export default { ...@@ -103,6 +106,17 @@ export default {
(!this.mr.isNothingToMergeState && !this.mr.isMergedState) (!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() { created() {
this.initPolling(); this.initPolling();
...@@ -112,11 +126,19 @@ export default { ...@@ -112,11 +126,19 @@ export default {
mounted() { mounted() {
this.setFaviconHelper(); this.setFaviconHelper();
this.initDeploymentsPolling(); this.initDeploymentsPolling();
if (this.shouldRenderMergedPipeline) {
this.initPostMergeDeploymentsPolling();
}
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus); eventHub.$off('mr.discussion.updated', this.checkStatus);
this.pollingInterval.destroy(); this.pollingInterval.destroy();
this.deploymentsInterval.destroy(); this.deploymentsInterval.destroy();
if (this.postMergeDeploymentsInterval) {
this.postMergeDeploymentsInterval.destroy();
}
}, },
methods: { methods: {
createService(store) { createService(store) {
...@@ -146,7 +168,13 @@ export default { ...@@ -146,7 +168,13 @@ export default {
cb.call(null, data); 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() { initPolling() {
this.pollingInterval = new SmartInterval({ this.pollingInterval = new SmartInterval({
...@@ -158,8 +186,14 @@ export default { ...@@ -158,8 +186,14 @@ export default {
}); });
}, },
initDeploymentsPolling() { initDeploymentsPolling() {
this.deploymentsInterval = new SmartInterval({ this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments);
callback: this.fetchDeployments, },
initPostMergeDeploymentsPolling() {
this.postMergeDeploymentsInterval = this.deploymentsPoll(this.fetchPostMergeDeployments);
},
deploymentsPoll(callback) {
return new SmartInterval({
callback,
startingInterval: 30000, startingInterval: 30000,
maxInterval: 120000, maxInterval: 120000,
hiddenInterval: 240000, hiddenInterval: 240000,
...@@ -167,26 +201,29 @@ export default { ...@@ -167,26 +201,29 @@ export default {
immediateExecution: true, immediateExecution: true,
}); });
}, },
setFaviconHelper() { fetchDeployments(target) {
if (this.mr.ciStatusFaviconPath) { return this.service.fetchDeployments(target);
return setFaviconOverlay(this.mr.ciStatusFaviconPath);
}
return Promise.resolve();
}, },
fetchDeployments() { fetchPreMergeDeployments() {
return this.service return this.fetchDeployments()
.fetchDeployments() .then(({ data }) => {
.then(res => res.data)
.then(data => {
if (data.length) { if (data.length) {
this.mr.deployments = data; this.mr.deployments = data;
} }
}) })
.catch(() => { .catch(() => this.throwDeploymentsError());
createFlash( },
'Something went wrong while fetching the environments for this merge request. Please try again.', 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() { fetchActionsContent() {
this.service this.service
...@@ -199,7 +236,7 @@ export default { ...@@ -199,7 +236,7 @@ export default {
Project.initRefSwitcher(); Project.initRefSwitcher();
} }
}) })
.catch(() => createFlash('Something went wrong. Please try again.')); .catch(() => createFlash(__('Something went wrong. Please try again.')));
}, },
handleNotification(data) { handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return; if (data.ci_status === this.mr.ciStatus) return;
...@@ -267,7 +304,8 @@ export default { ...@@ -267,7 +304,8 @@ export default {
/> />
<deployment <deployment
v-for="deployment in mr.deployments" v-for="deployment in mr.deployments"
:key="deployment.id" :key="`pre-merge-deploy-${deployment.id}`"
class="js-pre-merge-deploy"
:deployment="deployment" :deployment="deployment"
/> />
<div class="mr-section-container"> <div class="mr-section-container">
...@@ -308,5 +346,22 @@ export default { ...@@ -308,5 +346,22 @@ export default {
<mr-widget-merge-help /> <mr-widget-merge-help />
</div> </div>
</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> </div>
</template> </template>
...@@ -21,8 +21,12 @@ export default class MRWidgetService { ...@@ -21,8 +21,12 @@ export default class MRWidgetService {
return axios.delete(this.endpoints.sourceBranchPath); return axios.delete(this.endpoints.sourceBranchPath);
} }
fetchDeployments() { fetchDeployments(targetParam) {
return axios.get(this.endpoints.ciEnvironmentsStatusPath); return axios.get(this.endpoints.ciEnvironmentsStatusPath, {
params: {
environment_target: targetParam
}
});
} }
poll() { poll() {
......
...@@ -32,7 +32,9 @@ export default class MergeRequestStore { ...@@ -32,7 +32,9 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count; this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count; this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {}; this.pipeline = data.pipeline || {};
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || []; this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
this.initRebase(data); this.initRebase(data);
if (data.issues_links) { if (data.issues_links) {
......
...@@ -201,8 +201,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -201,8 +201,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end end
def ci_environments_status def ci_environments_status
environments = @merge_request.environments_for(current_user).map do |environment| environments = if ci_environments_status_on_merge_result?
EnvironmentStatus.new(environment, @merge_request) EnvironmentStatus.after_merge_request(@merge_request, current_user)
else
EnvironmentStatus.for_merge_request(@merge_request, current_user)
end end
render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments) render json: EnvironmentStatusSerializer.new(current_user: current_user).represent(environments)
...@@ -241,6 +243,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -241,6 +243,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private private
def ci_environments_status_on_merge_result?
params[:environment_target] == 'merge_commit'
end
def target_branch_missing? def target_branch_missing?
@merge_request.has_no_commits? && !@merge_request.target_branch_exists? @merge_request.has_no_commits? && !@merge_request.target_branch_exists?
end end
......
...@@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base ...@@ -127,6 +127,10 @@ class Deployment < ActiveRecord::Base
metrics&.merge(deployment_time: created_at.to_i) || {} metrics&.merge(deployment_time: created_at.to_i) || {}
end end
def status
'success'
end
private private
def prometheus_adapter def prometheus_adapter
......
...@@ -3,21 +3,33 @@ ...@@ -3,21 +3,33 @@
class EnvironmentStatus class EnvironmentStatus
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :environment, :merge_request attr_reader :environment, :merge_request, :sha
delegate :id, to: :environment delegate :id, to: :environment
delegate :name, to: :environment delegate :name, to: :environment
delegate :project, to: :environment delegate :project, to: :environment
delegate :deployed_at, to: :deployment, allow_nil: true 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 @environment = environment
@merge_request = merge_request @merge_request = merge_request
@sha = sha
end end
def deployment def deployment
strong_memoize(:deployment) do strong_memoize(:deployment) do
environment.first_deployment_for(merge_request.diff_head_sha) environment.first_deployment_for(sha)
end end
end end
...@@ -26,10 +38,9 @@ class EnvironmentStatus ...@@ -26,10 +38,9 @@ class EnvironmentStatus
end end
def changes def changes
sha = merge_request.diff_head_sha
return [] if project.route_map_for(sha).nil? 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 end
def changed_files def changed_files
...@@ -41,7 +52,7 @@ class EnvironmentStatus ...@@ -41,7 +52,7 @@ class EnvironmentStatus
PAGE_EXTENSIONS = /\A\.(s?html?|php|asp|cgi|pl)\z/i.freeze 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) public_path = project.public_path_for_source_path(file.new_path, sha)
return if public_path.nil? return if public_path.nil?
...@@ -53,4 +64,22 @@ class EnvironmentStatus ...@@ -53,4 +64,22 @@ class EnvironmentStatus
external_url: environment.external_url_for(file.new_path, sha) external_url: environment.external_url_for(file.new_path, sha)
} }
end 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 end
...@@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -204,6 +204,12 @@ class MergeRequest < ActiveRecord::Base
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end 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 # Pattern used to extract `!123` merge request references from text
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
......
...@@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity ...@@ -5,6 +5,7 @@ class EnvironmentStatusEntity < Grape::Entity
expose :id expose :id
expose :name expose :name
expose :status
expose :url do |es| expose :url do |es|
project_environment_path(es.project, es.environment) project_environment_path(es.project, es.environment)
......
...@@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -55,6 +55,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_commit_message expose :merge_commit_message
expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline 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 # Booleans
expose :merge_ongoing?, as: :merge_ongoing 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 "" ...@@ -648,6 +648,9 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?" msgid "Are you sure you want to reset the health check token?"
msgstr "" msgstr ""
msgid "Are you sure you want to stop this environment?"
msgstr ""
msgid "Are you sure?" msgid "Are you sure?"
msgstr "" msgstr ""
...@@ -2324,6 +2327,12 @@ msgstr "" ...@@ -2324,6 +2327,12 @@ msgstr ""
msgid "DeployTokens|Your new project deploy token has been created." msgid "DeployTokens|Your new project deploy token has been created."
msgstr "" msgstr ""
msgid "Deployed to"
msgstr ""
msgid "Deploying to"
msgstr ""
msgid "Deprioritize label" msgid "Deprioritize label"
msgstr "" msgstr ""
...@@ -2750,6 +2759,9 @@ msgstr "" ...@@ -2750,6 +2759,9 @@ msgstr ""
msgid "Failed to check related branches." msgid "Failed to check related branches."
msgstr "" msgstr ""
msgid "Failed to deploy to"
msgstr ""
msgid "Failed to load emoji list." msgid "Failed to load emoji list."
msgstr "" msgstr ""
...@@ -5604,6 +5616,9 @@ msgstr "" ...@@ -5604,6 +5616,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again." msgid "Something went wrong while fetching comments. Please try again."
msgstr "" 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." msgid "Something went wrong while fetching the projects."
msgstr "" msgstr ""
......
...@@ -749,13 +749,15 @@ describe Projects::MergeRequestsController do ...@@ -749,13 +749,15 @@ describe Projects::MergeRequestsController do
describe 'GET ci_environments_status' do describe 'GET ci_environments_status' do
context 'the environment is from a forked project' do context 'the environment is from a forked project' do
let!(:forked) { fork_project(project, user, repository: true) } let(:forked) { fork_project(project, user, repository: true) }
let!(:environment) { create(:environment, project: forked) } let(:sha) { forked.commit.sha }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } let(:environment) { create(:environment, project: forked) }
let(:admin) { create(:admin) } 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 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 end
it 'links to the environment on that project' do it 'links to the environment on that project' do
...@@ -764,6 +766,35 @@ describe Projects::MergeRequestsController do ...@@ -764,6 +766,35 @@ describe Projects::MergeRequestsController do
expect(json_response.first['url']).to match /#{forked.full_path}/ expect(json_response.first['url']).to match /#{forked.full_path}/
end 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. # 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 # set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-ce/issues/52287
it 'keeps queries in check' do it 'keeps queries in check' do
...@@ -772,11 +803,15 @@ describe Projects::MergeRequestsController do ...@@ -772,11 +803,15 @@ describe Projects::MergeRequestsController do
expect(control_count).to be <= 137 expect(control_count).to be <= 137
end end
def get_ci_environments_status def get_ci_environments_status(extra_params = {})
get :ci_environments_status, params = {
namespace_id: merge_request.project.namespace.to_param, namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project, 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 end
end end
......
...@@ -3,15 +3,19 @@ require 'rails_helper' ...@@ -3,15 +3,19 @@ require 'rails_helper'
describe 'Merge request > User sees deployment widget', :js do describe 'Merge request > User sees deployment widget', :js do
describe 'when deployed to an environment' do describe 'when deployed to an environment' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { merge_request.target_project } let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, :merged) } let(:merge_request) { create(:merge_request, :merged, source_project: project) }
let(:environment) { create(:environment, project: project) } let(:environment) { create(:environment, project: project) }
let(:role) { :developer } let(:role) { :developer }
let(:sha) { project.commit('master').id } let(:ref) { merge_request.target_branch }
let!(:deployment) { create(:deployment, environment: environment, sha: sha) } 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) { } let!(:manual) { }
before do before do
merge_request.update!(merge_commit_sha: sha)
project.add_user(user, role) project.add_user(user, role)
sign_in(user) sign_in(user)
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
...@@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do ...@@ -26,15 +30,10 @@ describe 'Merge request > User sees deployment widget', :js do
end end
context 'with stop action' do 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(: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 before do
deployment.update!(on_stop: manual.name)
wait_for_requests wait_for_requests
end end
......
...@@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do ...@@ -40,21 +40,26 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request' do context 'view merge request' do
let!(:environment) { create(:environment, project: project) } 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 let!(:deployment) do
create(:deployment, environment: environment, create(:deployment, environment: environment,
ref: 'feature', ref: merge_request.source_branch,
sha: merge_request.diff_head_sha) deployable: build,
sha: sha)
end end
before do before do
merge_request.update!(head_pipeline: pipeline)
visit project_merge_request_path(project, merge_request) visit project_merge_request_path(project, merge_request)
end end
it 'shows environments link' do it 'shows environments link' do
wait_for_requests 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(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url) expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end end
......
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
"diff_head_commit_short_id": { "type": ["string", "null"] }, "diff_head_commit_short_id": { "type": ["string", "null"] },
"merge_commit_message": { "type": ["string", "null"] }, "merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] }, "pipeline": { "type": ["object", "null"] },
"merge_pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" }, "work_in_progress": { "type": "boolean" },
"source_branch_exists": { "type": "boolean" }, "source_branch_exists": { "type": "boolean" },
"mergeable_discussions_state": { "type": "boolean" }, "mergeable_discussions_state": { "type": "boolean" },
......
...@@ -2,16 +2,19 @@ import Vue from 'vue'; ...@@ -2,16 +2,19 @@ import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue'; import deploymentComponent from '~/vue_merge_request_widget/components/deployment.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import mountComponent from '../../helpers/vue_mount_component_helper';
const deploymentMockData = { describe('Deployment component', () => {
const Component = Vue.extend(deploymentComponent);
const deploymentMockData = {
id: 15, id: 15,
name: 'review/diplo', name: 'review/diplo',
url: '/root/acets-review-apps/environments/15', url: '/root/review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop', stop_url: '/root/review-apps/environments/15/stop',
metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics', metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
external_url: 'http://diplo.', external_url: 'http://gitlab.com.',
external_url_formatted: 'diplo.', external_url_formatted: 'gitlab',
deployed_at: '2017-03-22T22:44:42.258Z', deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm', deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes: [ changes: [
...@@ -28,28 +31,19 @@ const deploymentMockData = { ...@@ -28,28 +31,19 @@ const deploymentMockData = {
external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/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 } },
});
};
describe('Deployment component', () => {
let vm; let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
}); });
describe('computed', () => { describe('', () => {
beforeEach(() => {
vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
});
describe('deployTimeago', () => { describe('deployTimeago', () => {
it('return formatted date', () => { it('return formatted date', () => {
const readable = getTimeago().format(deploymentMockData.deployed_at); const readable = getTimeago().format(deploymentMockData.deployed_at);
...@@ -111,9 +105,7 @@ describe('Deployment component', () => { ...@@ -111,9 +105,7 @@ describe('Deployment component', () => {
expect(vm.hasDeploymentMeta).toEqual(false); expect(vm.hasDeploymentMeta).toEqual(false);
}); });
}); });
});
describe('methods', () => {
describe('stopEnvironment', () => { describe('stopEnvironment', () => {
const url = '/foo/bar'; const url = '/foo/bar';
const returnPromise = () => const returnPromise = () =>
...@@ -152,42 +144,33 @@ describe('Deployment component', () => { ...@@ -152,42 +144,33 @@ describe('Deployment component', () => {
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled(); expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
}); });
}); });
});
describe('template', () => {
let el;
beforeEach(() => {
vm = createComponent(deploymentMockData);
el = vm.$el;
});
it('renders deployment name', () => { 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, 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', () => { 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, 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', () => { it('renders stop button', () => {
expect(el.querySelector('.btn')).not.toBeNull(); expect(vm.$el.querySelector('.btn')).not.toBeNull();
}); });
it('renders deployment time', () => { 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', () => { 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', () => { ...@@ -196,8 +179,7 @@ describe('Deployment component', () => {
window.gon = window.gon || {}; window.gon = window.gon || {};
window.gon.features = window.gon.features || {}; window.gon.features = window.gon.features || {};
window.gon.features.ciEnvironmentsStatusChanges = true; window.gon.features.ciEnvironmentsStatusChanges = true;
vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
vm = createComponent(deploymentMockData);
}); });
afterEach(() => { afterEach(() => {
...@@ -216,7 +198,7 @@ describe('Deployment component', () => { ...@@ -216,7 +198,7 @@ describe('Deployment component', () => {
window.gon.features = window.gon.features || {}; window.gon.features = window.gon.features || {};
window.gon.features.ciEnvironmentsStatusChanges = false; window.gon.features.ciEnvironmentsStatusChanges = false;
vm = createComponent(deploymentMockData); vm = mountComponent(Component, { deployment: { ...deploymentMockData } });
}); });
afterEach(() => { afterEach(() => {
...@@ -228,4 +210,44 @@ describe('Deployment component', () => { ...@@ -228,4 +210,44 @@ describe('Deployment component', () => {
expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull(); 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',
);
});
});
});
}); });
...@@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => { ...@@ -189,7 +189,7 @@ describe('mrWidgetOptions', () => {
it('should fetch deployments', done => { it('should fetch deployments', done => {
spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }])); spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ id: 1 }]));
vm.fetchDeployments(); vm.fetchPreMergeDeployments();
setTimeout(() => { setTimeout(() => {
expect(vm.service.fetchDeployments).toHaveBeenCalled(); expect(vm.service.fetchDeployments).toHaveBeenCalled();
...@@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => { ...@@ -454,6 +454,7 @@ describe('mrWidgetOptions', () => {
deployed_at: '2017-03-22T22:44:42.258Z', deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm', deployed_at_formatted: 'Mar 22, 2017 10:44pm',
changes, changes,
status: 'success'
}; };
beforeEach(done => { beforeEach(done => {
...@@ -486,4 +487,189 @@ describe('mrWidgetOptions', () => { ...@@ -486,4 +487,189 @@ describe('mrWidgetOptions', () => {
).toEqual(changes.length); ).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();
});
});
});
}); });
...@@ -5,13 +5,15 @@ describe EnvironmentStatus do ...@@ -5,13 +5,15 @@ describe EnvironmentStatus do
let(:environment) { deployment.environment} let(:environment) { deployment.environment}
let(:project) { deployment.project } let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } 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(:id).to(:environment) }
it { is_expected.to delegate_method(:name).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(:project).to(:environment) }
it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) } 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 describe '#project' do
subject { environment_status.project } subject { environment_status.project }
...@@ -58,4 +60,32 @@ describe EnvironmentStatus do ...@@ -58,4 +60,32 @@ describe EnvironmentStatus do
) )
end end
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 end
...@@ -1058,6 +1058,26 @@ describe MergeRequest do ...@@ -1058,6 +1058,26 @@ describe MergeRequest do
end end
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 describe '#has_ci?' do
let(:merge_request) { build_stubbed(:merge_request) } let(:merge_request) { build_stubbed(:merge_request) }
......
...@@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do ...@@ -9,7 +9,7 @@ describe EnvironmentStatusEntity do
let(:project) { deployment.project } let(:project) { deployment.project }
let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } 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) } let(:entity) { described_class.new(environment_status, request: request) }
subject { entity.as_json } subject { entity.as_json }
...@@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do ...@@ -26,6 +26,7 @@ describe EnvironmentStatusEntity do
it { is_expected.to include(:deployed_at) } it { is_expected.to include(:deployed_at) }
it { is_expected.to include(:deployed_at_formatted) } it { is_expected.to include(:deployed_at_formatted) }
it { is_expected.to include(:changes) } 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(:stop_url) }
it { is_expected.not_to include(:metrics_url) } it { is_expected.not_to include(:metrics_url) }
......
...@@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do ...@@ -52,6 +52,40 @@ describe MergeRequestWidgetEntity do
end end
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 describe 'metrics' do
context 'when metrics record exists with merged data' do context 'when metrics record exists with merged data' do
before do before do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment