Skip to content
Snippets Groups Projects
Commit 7c38405b authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 1fa79760
No related branches found
No related tags found
No related merge requests found
Showing
with 388 additions and 152 deletions
Loading
Loading
@@ -55,7 +55,7 @@ eslint-report.html
/dump.rdb
/jsconfig.json
/log/*.log*
/node_modules/
/node_modules
/nohup.out
/public/assets/
/public/uploads.*
Loading
Loading
Loading
Loading
@@ -6,3 +6,8 @@ export const RUNNING = 'running';
export const SUCCESS = 'success';
export const FAILED = 'failed';
export const CANCELED = 'canceled';
// ACTION STATUSES
export const STOPPING = 'stopping';
export const DEPLOYING = 'deploying';
export const REDEPLOYING = 'redeploying';
<script>
import { __, s__ } from '~/locale';
import DeploymentActions from './deployment_actions.vue';
import DeploymentInfo from './deployment_info.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import DeploymentStopButton from './deployment_stop_button.vue';
import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED, RUNNING, SUCCESS } from './constants';
import { MANUAL_DEPLOY, WILL_DEPLOY, CREATED } from './constants';
 
export default {
// name: 'Deployment' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Deployment',
components: {
DeploymentActions,
DeploymentInfo,
DeploymentStopButton,
DeploymentViewButton,
},
props: {
deployment: {
Loading
Loading
@@ -40,38 +37,14 @@ export default {
},
},
computed: {
appButtonText() {
return {
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
tooltip: this.isCurrent
? ''
: __('View the latest successful deployment to this environment'),
};
},
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY;
},
computedDeploymentStatus() {
if (this.deployment.status === CREATED) {
return this.isManual ? MANUAL_DEPLOY : WILL_DEPLOY;
}
return this.deployment.status;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
isManual() {
return Boolean(
this.deployment.details &&
this.deployment.details.playable_build &&
this.deployment.details.playable_build.play_path,
);
},
isDeployInProgress() {
return this.deployment.status === RUNNING;
return Boolean(this.deployment.details?.playable_build?.play_path);
},
},
};
Loading
Loading
@@ -87,22 +60,12 @@ export default {
:deployment="deployment"
:show-metrics="showMetrics"
/>
<div>
<!-- show appropriate version of review app button -->
<deployment-view-button
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
<!-- if it is stoppable, show stop -->
<deployment-stop-button
v-if="deployment.stop_url"
:is-deploy-in-progress="isDeployInProgress"
:stop-url="deployment.stop_url"
/>
</div>
<deployment-actions
:deployment="deployment"
:computed-deployment-status="computedDeploymentStatus"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
</div>
</div>
</div>
Loading
Loading
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import { RUNNING } from './constants';
export default {
name: 'DeploymentActionButton',
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
actionsConfiguration: {
type: Object,
required: true,
},
actionInProgress: {
type: String,
required: false,
default: null,
},
buttonTitle: {
type: String,
required: false,
default: '',
},
computedDeploymentStatus: {
type: String,
required: true,
},
containerClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
isActionInProgress() {
return Boolean(this.computedDeploymentStatus === RUNNING || this.actionInProgress);
},
actionInProgressTooltip() {
switch (this.actionInProgress) {
case this.actionsConfiguration.actionName:
return this.actionsConfiguration.busyText;
case null:
return '';
default:
return __('Another action is currently in progress');
}
},
isLoading() {
return this.actionInProgress === this.actionsConfiguration.actionName;
},
},
};
</script>
<template>
<span v-gl-tooltip :title="actionInProgressTooltip" class="d-inline-block" tabindex="0">
<gl-button
v-gl-tooltip
:title="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
:class="`btn btn-default btn-sm inline prepend-left-4 ${containerClasses}`"
@click="$emit('click')"
>
<span class="d-inline-flex align-items-baseline">
<slot> </slot>
</span>
</gl-button>
</span>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
import DeploymentActionButton from './deployment_action_button.vue';
import DeploymentViewButton from './deployment_view_button.vue';
import { MANUAL_DEPLOY, FAILED, SUCCESS, STOPPING, DEPLOYING, REDEPLOYING } from './constants';
export default {
name: 'DeploymentActions',
components: {
DeploymentActionButton,
DeploymentViewButton,
GlIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
computedDeploymentStatus: {
type: String,
required: true,
},
deployment: {
type: Object,
required: true,
},
showVisualReviewApp: {
type: Boolean,
required: false,
default: false,
},
visualReviewAppMeta: {
type: Object,
required: false,
default: () => ({
sourceProjectId: '',
sourceProjectPath: '',
mergeRequestId: '',
appUrl: '',
}),
},
},
data() {
return {
actionInProgress: null,
constants: {
STOPPING,
DEPLOYING,
REDEPLOYING,
},
};
},
computed: {
appButtonText() {
return {
text: this.isCurrent ? s__('Review App|View app') : s__('Review App|View latest app'),
tooltip: this.isCurrent
? ''
: __('View the latest successful deployment to this environment'),
};
},
canBeManuallyDeployed() {
return this.computedDeploymentStatus === MANUAL_DEPLOY && Boolean(this.playPath);
},
canBeManuallyRedeployed() {
return this.computedDeploymentStatus === FAILED && Boolean(this.redeployPath);
},
shouldShowManualButtons() {
return this.glFeatures.deployFromFooter;
},
hasExternalUrls() {
return Boolean(this.deployment.external_url && this.deployment.external_url_formatted);
},
isCurrent() {
return this.computedDeploymentStatus === SUCCESS;
},
playPath() {
return this.deployment.details?.playable_build?.play_path;
},
redeployPath() {
return this.deployment.details?.playable_build?.retry_path;
},
stopUrl() {
return this.deployment.stop_url;
},
},
actionsConfiguration: {
[STOPPING]: {
actionName: STOPPING,
buttonText: s__('MrDeploymentActions|Stop environment'),
busyText: __('This environment is being deployed'),
confirmMessage: __('Are you sure you want to stop this environment?'),
errorMessage: __('Something went wrong while stopping this environment. Please try again.'),
},
[DEPLOYING]: {
actionName: DEPLOYING,
buttonText: s__('MrDeploymentActions|Deploy'),
busyText: __('This environment is being deployed'),
confirmMessage: __('Are you sure you want to deploy this environment?'),
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
},
[REDEPLOYING]: {
actionName: REDEPLOYING,
buttonText: s__('MrDeploymentActions|Re-deploy'),
busyText: __('This environment is being re-deployed'),
confirmMessage: __('Are you sure you want to re-deploy this environment?'),
errorMessage: __('Something went wrong while deploying this environment. Please try again.'),
},
},
methods: {
executeAction(endpoint, { actionName, confirmMessage, errorMessage }) {
const isConfirmed = confirm(confirmMessage); //eslint-disable-line
if (isConfirmed) {
this.actionInProgress = actionName;
MRWidgetService.executeInlineAction(endpoint)
.then(resp => {
const redirectUrl = resp?.data?.redirect_url;
if (redirectUrl) {
visitUrl(redirectUrl);
}
})
.catch(() => {
createFlash(errorMessage);
})
.finally(() => {
this.actionInProgress = null;
});
}
},
stopEnvironment() {
this.executeAction(this.stopUrl, this.$options.actionsConfiguration[STOPPING]);
},
deployManually() {
this.executeAction(this.playPath, this.$options.actionsConfiguration[DEPLOYING]);
},
redeploy() {
this.executeAction(this.redeployPath, this.$options.actionsConfiguration[REDEPLOYING]);
},
},
};
</script>
<template>
<div>
<deployment-action-button
v-if="shouldShowManualButtons && canBeManuallyDeployed"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.DEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
container-classes="js-manual-deploy-action"
@click="deployManually"
>
<gl-icon name="play" />
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-action-button
v-if="shouldShowManualButtons && canBeManuallyRedeployed"
:action-in-progress="actionInProgress"
:actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
:computed-deployment-status="computedDeploymentStatus"
container-classes="js-manual-redeploy-action"
@click="redeploy"
>
<gl-icon name="repeat" />
<span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
</deployment-action-button>
<deployment-view-button
v-if="hasExternalUrls"
:app-button-text="appButtonText"
:deployment="deployment"
:show-visual-review-app="showVisualReviewApp"
:visual-review-app-metadata="visualReviewAppMeta"
/>
<deployment-action-button
v-if="stopUrl"
:action-in-progress="actionInProgress"
:computed-deployment-status="computedDeploymentStatus"
:actions-configuration="$options.actionsConfiguration[constants.STOPPING]"
:button-title="$options.actionsConfiguration[constants.STOPPING].buttonText"
container-classes="js-stop-env"
@click="stopEnvironment"
>
<gl-icon name="stop" />
</deployment-action-button>
</div>
</template>
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import MRWidgetService from '../../services/mr_widget_service';
export default {
name: 'DeploymentStopButton',
components: {
LoadingButton,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
isDeployInProgress: {
type: Boolean,
required: true,
},
stopUrl: {
type: String,
required: true,
},
},
data() {
return {
isStopping: false,
};
},
computed: {
deployInProgressTooltip() {
return this.isDeployInProgress
? __('Stopping this environment is currently not possible as a deployment is in progress')
: '';
},
},
methods: {
stopEnvironment() {
const msg = __('Are you sure you want to stop this environment?');
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
this.isStopping = true;
MRWidgetService.stopEnvironment(this.stopUrl)
.then(res => res.data)
.then(data => {
if (data.redirect_url) {
visitUrl(data.redirect_url);
}
this.isStopping = false;
})
.catch(() => {
createFlash(
__('Something went wrong while stopping this environment. Please try again.'),
);
this.isStopping = false;
});
}
},
},
};
</script>
<template>
<span v-gl-tooltip :title="deployInProgressTooltip" class="d-inline-block" tabindex="0">
<loading-button
v-gl-tooltip
:loading="isStopping"
:disabled="isDeployInProgress"
:title="__('Stop environment')"
container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4"
@click="stopEnvironment"
>
<icon name="stop" />
</loading-button>
</span>
</template>
Loading
Loading
@@ -54,7 +54,7 @@ export default class MRWidgetService {
return axios.post(this.endpoints.rebasePath);
}
 
static stopEnvironment(url) {
static executeInlineAction(url) {
return axios.post(url);
}
 
Loading
Loading
Loading
Loading
@@ -20,6 +20,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do
push_frontend_feature_flag(:diffs_batch_load, @project, default_enabled: true)
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
push_frontend_feature_flag(:single_mr_diff_view, @project)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end
Loading
Loading
Loading
Loading
@@ -141,3 +141,5 @@ class GitlabSchema < GraphQL::Schema
end
end
end
GitlabSchema.prepend_if_ee('EE::GitlabSchema')
Loading
Loading
@@ -43,11 +43,7 @@ module MergeRequests
abort_auto_merge(merge_request, 'target branch was changed')
end
 
if merge_request.assignees != old_assignees
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
handle_assignees_change(merge_request, old_assignees) if merge_request.assignees != old_assignees
 
if merge_request.previous_changes.include?('target_branch') ||
merge_request.previous_changes.include?('source_branch')
Loading
Loading
@@ -120,6 +116,12 @@ module MergeRequests
end
end
 
def handle_assignees_change(merge_request, old_assignees)
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
Loading
Loading
---
title: Add deploy and re-deploy buttons to deployments
merge_request: 25427
author:
type: added
# frozen_string_literal: true
class AddMergeRequestMetricsFirstReassignedAt < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :merge_request_metrics, :first_reassigned_at, :datetime_with_timezone
end
end
def down
with_lock_retries do
remove_column :merge_request_metrics, :first_reassigned_at, :datetime_with_timezone
end
end
end
# frozen_string_literal: true
class AddMergeRequestAssigneeCreatedAt < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :merge_request_assignees, :created_at, :datetime_with_timezone
end
end
def down
with_lock_retries do
remove_column :merge_request_assignees, :created_at
end
end
end
Loading
Loading
@@ -2450,6 +2450,7 @@ ActiveRecord::Schema.define(version: 2020_03_06_170531) do
create_table "merge_request_assignees", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "merge_request_id", null: false
t.datetime_with_timezone "created_at"
t.index ["merge_request_id", "user_id"], name: "index_merge_request_assignees_on_merge_request_id_and_user_id", unique: true
t.index ["merge_request_id"], name: "index_merge_request_assignees_on_merge_request_id"
t.index ["user_id"], name: "index_merge_request_assignees_on_user_id"
Loading
Loading
@@ -2564,6 +2565,7 @@ ActiveRecord::Schema.define(version: 2020_03_06_170531) do
t.integer "modified_paths_size"
t.integer "commits_count"
t.datetime_with_timezone "first_approved_at"
t.datetime_with_timezone "first_reassigned_at"
t.index ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at"
t.index ["latest_closed_at"], name: "index_merge_request_metrics_on_latest_closed_at", where: "(latest_closed_at IS NOT NULL)"
t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id"
Loading
Loading
Loading
Loading
@@ -1948,8 +1948,8 @@ type Epic implements Noteable {
descendantCounts: EpicDescendantCount
 
"""
Total weight of open and closed descendant epic's issues. Available only when
feature flag `unfiltered_epic_aggregates` is enabled.
Total weight of open and closed issues in the epic and its descendants.
Available only when feature flag `unfiltered_epic_aggregates` is enabled.
"""
descendantWeightSum: EpicDescendantWeights
 
Loading
Loading
Loading
Loading
@@ -317,7 +317,7 @@ Represents an epic.
| `closedAt` | Time | Timestamp of the epic's closure |
| `createdAt` | Time | Timestamp of the epic's creation |
| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed descendant epic's issues. Available only when feature flag `unfiltered_epic_aggregates` is enabled. |
| `descendantWeightSum` | EpicDescendantWeights | Total weight of open and closed issues in the epic and its descendants. Available only when feature flag `unfiltered_epic_aggregates` is enabled. |
| `description` | String | Description of the epic |
| `downvotes` | Int! | Number of downvotes the epic has received |
| `dueDate` | Time | Due date of the epic |
Loading
Loading
Loading
Loading
@@ -29,6 +29,9 @@ The squashed commit's commit message will be either:
- Taken from the first multi-line commit message in the merge.
- The merge request's title if no multi-line commit message is found.
 
NOTE: **Note:**
This only takes effect if there are at least 2 commits. As there is nothing to squash, the commit message does not change if there is only 1 commit.
It can be customized before merging a merge request.
 
![A squash commit message editor](img/squash_mr_message.png)
Loading
Loading
Loading
Loading
@@ -200,7 +200,9 @@ module Gitlab
end
 
def subject_starts_with_lowercase?
first_char = subject[0]
first_char = subject.sub(/\A\[.+\]\s/, '')[0]
first_char_downcased = first_char.downcase
return true unless ('a'..'z').cover?(first_char_downcased)
 
first_char.downcase == first_char
end
Loading
Loading
Loading
Loading
@@ -2038,6 +2038,9 @@ msgstr ""
msgid "Anonymous"
msgstr ""
 
msgid "Another action is currently in progress"
msgstr ""
msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time"
msgstr ""
 
Loading
Loading
@@ -2283,6 +2286,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone."
msgstr ""
 
msgid "Are you sure you want to deploy this environment?"
msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
 
Loading
Loading
@@ -2298,6 +2304,9 @@ msgstr ""
msgid "Are you sure you want to permanently delete this license?"
msgstr ""
 
msgid "Are you sure you want to re-deploy this environment?"
msgstr ""
msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again."
msgstr ""
 
Loading
Loading
@@ -12799,6 +12808,15 @@ msgstr ""
msgid "Moves this issue to %{path_to_project}."
msgstr ""
 
msgid "MrDeploymentActions|Deploy"
msgstr ""
msgid "MrDeploymentActions|Re-deploy"
msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multiple issue boards"
msgstr ""
 
Loading
Loading
@@ -18243,6 +18261,9 @@ msgstr ""
msgid "Something went wrong while deleting your note. Please try again."
msgstr ""
 
msgid "Something went wrong while deploying this environment. Please try again."
msgstr ""
msgid "Something went wrong while editing your comment. Please try again."
msgstr ""
 
Loading
Loading
@@ -18741,9 +18762,6 @@ msgstr ""
msgid "Stop Terminal"
msgstr ""
 
msgid "Stop environment"
msgstr ""
msgid "Stop impersonation"
msgstr ""
 
Loading
Loading
@@ -18753,9 +18771,6 @@ msgstr ""
msgid "Stopped"
msgstr ""
 
msgid "Stopping this environment is currently not possible as a deployment is in progress"
msgstr ""
msgid "Stopping..."
msgstr ""
 
Loading
Loading
@@ -20002,6 +20017,12 @@ msgstr ""
msgid "This environment has no deployments yet."
msgstr ""
 
msgid "This environment is being deployed"
msgstr ""
msgid "This environment is being re-deployed"
msgstr ""
msgid "This epic already has the maximum number of child epics."
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -81,10 +81,13 @@ class AutomatedCleanup
release = Quality::HelmClient::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, review_apps_namespace)
releases_to_delete << release
end
elsif environment.state != 'stopped' && deployed_at < stop_threshold
stop_environment(environment, deployment)
else
print_release_state(subject: 'Review App', release_name: environment.slug, release_date: last_deploy, action: 'leaving')
if deployed_at >= stop_threshold
print_release_state(subject: 'Review App', release_name: environment.slug, release_date: last_deploy, action: 'leaving')
else
environment_state = fetch_environment(environment)&.state
stop_environment(environment, deployment) if environment_state && environment_state != 'stopped'
end
end
 
checked_environments << environment.slug
Loading
Loading
@@ -116,12 +119,19 @@ class AutomatedCleanup
 
private
 
def fetch_environment(environment)
gitlab.environment(project_path, environment.id)
rescue Errno::ETIMEDOUT => ex
puts "Failed to fetch '#{environment.name}' / '#{environment.slug}' (##{environment.id}):\n#{ex.message}"
nil
end
def delete_environment(environment, deployment)
print_release_state(subject: 'Review app', release_name: environment.slug, release_date: deployment.created_at, action: 'deleting')
gitlab.delete_environment(project_path, environment.id)
 
rescue Gitlab::Error::Forbidden
puts "Review app '#{environment.slug}' is forbidden: skipping it"
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) is forbidden: skipping it"
end
 
def stop_environment(environment, deployment)
Loading
Loading
@@ -129,7 +139,7 @@ class AutomatedCleanup
gitlab.stop_environment(project_path, environment.id)
 
rescue Gitlab::Error::Forbidden
puts "Review app '#{environment.slug}' is forbidden: skipping it"
puts "Review app '#{environment.name}' / '#{environment.slug}' (##{environment.id}) is forbidden: skipping it"
end
 
def helm_releases
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment