summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-16 18:08:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-16 18:08:01 +0000
commit8e45d25f7dde6508839ffee719c0ddc2cf6b12d3 (patch)
tree9839e7fe63b36904d40995ebf519124c9a8f7681 /app
parent00c78fb814d7ce00989ac04edd6cdaa3239da284 (diff)
downloadgitlab-ce-8e45d25f7dde6508839ffee719c0ddc2cf6b12d3.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js17
-rw-r--r--app/assets/javascripts/pages/projects/releases/edit/index.js7
-rw-r--r--app/assets/javascripts/releases/detail/components/app.vue156
-rw-r--r--app/assets/javascripts/releases/detail/index.js19
-rw-r--r--app/assets/javascripts/releases/detail/store/actions.js62
-rw-r--r--app/assets/javascripts/releases/detail/store/index.js14
-rw-r--r--app/assets/javascripts/releases/detail/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/releases/detail/store/mutations.js42
-rw-r--r--app/assets/javascripts/releases/detail/store/state.js15
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/pages/tags.scss3
-rw-r--r--app/controllers/health_controller.rb21
-rw-r--r--app/controllers/projects/deployments_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb8
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/environment_helper.rb33
-rw-r--r--app/helpers/releases_helper.rb10
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/deployment.rb8
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/project.rb2
-rw-r--r--app/policies/deployment_policy.rb14
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/services/deployments/after_create_service.rb60
-rw-r--r--app/services/deployments/create_service.rb39
-rw-r--r--app/services/deployments/update_service.rb16
-rw-r--r--app/services/git/base_hooks_service.rb2
-rw-r--r--app/services/git/process_ref_changes_service.rb11
-rw-r--r--app/services/update_deployment_service.rb57
-rw-r--r--app/views/admin/application_settings/_performance.html.haml5
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml38
-rw-r--r--app/views/projects/deployments/_rollback.haml2
-rw-r--r--app/views/projects/environments/show.html.haml9
-rw-r--r--app/views/projects/issues/import_csv/_button.html.haml2
-rw-r--r--app/views/projects/releases/edit.html.haml3
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml6
-rw-r--r--app/workers/deployments/success_worker.rb2
40 files changed, 622 insertions, 98 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d57be10f472..908dc730aa4 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
+ releasePath: '/api/:version/projects/:id/releases/:tag_name',
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: 'api/:version/application/statistics',
@@ -391,6 +392,22 @@ const Api = {
return axios.get(url);
},
+ release(projectPath, tagName) {
+ const url = Api.buildUrl(this.releasePath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.get(url);
+ },
+
+ updateRelease(projectPath, tagName, release) {
+ const url = Api.buildUrl(this.releasePath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.put(url, release);
+ },
+
adminStatistics() {
const url = Api.buildUrl(this.adminStatisticsPath);
return axios.get(url);
diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js
new file mode 100644
index 00000000000..98ec196fc37
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/releases/edit/index.js
@@ -0,0 +1,7 @@
+import ZenMode from '~/zen_mode';
+import initEditRelease from '~/releases/detail';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ZenMode(); // eslint-disable-line no-new
+ initEditRelease();
+});
diff --git a/app/assets/javascripts/releases/detail/components/app.vue b/app/assets/javascripts/releases/detail/components/app.vue
new file mode 100644
index 00000000000..54a441de886
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/components/app.vue
@@ -0,0 +1,156 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+
+export default {
+ name: 'ReleaseDetailApp',
+ components: {
+ GlFormInput,
+ GlFormGroup,
+ GlButton,
+ MarkdownField,
+ },
+ directives: {
+ autofocusonshow,
+ },
+ computed: {
+ ...mapState([
+ 'isFetchingRelease',
+ 'fetchError',
+ 'markdownDocsPath',
+ 'markdownPreviewPath',
+ 'releasesPagePath',
+ ]),
+ showForm() {
+ return !this.isFetchingRelease && !this.fetchError;
+ },
+ subtitleText() {
+ return sprintf(
+ __(
+ 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}.',
+ ),
+ {
+ codeStart: '<code>',
+ codeEnd: '</code>',
+ },
+ false,
+ );
+ },
+ tagName() {
+ return this.$store.state.release.tagName;
+ },
+ releaseTitle: {
+ get() {
+ return this.$store.state.release.name;
+ },
+ set(title) {
+ this.updateReleaseTitle(title);
+ },
+ },
+ releaseNotes: {
+ get() {
+ return this.$store.state.release.description;
+ },
+ set(notes) {
+ this.updateReleaseNotes(notes);
+ },
+ },
+ },
+ created() {
+ this.fetchRelease();
+ },
+ methods: {
+ ...mapActions([
+ 'fetchRelease',
+ 'updateRelease',
+ 'updateReleaseTitle',
+ 'updateReleaseNotes',
+ 'navigateToReleasesPage',
+ ]),
+ },
+};
+</script>
+<template>
+ <div class="d-flex flex-column">
+ <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
+ <form v-if="showForm" @submit.prevent="updateRelease()">
+ <div class="row">
+ <gl-form-group class="col-md-6 col-lg-5 col-xl-4">
+ <label for="git-ref">{{ __('Tag name') }}</label>
+ <gl-form-input
+ id="git-ref"
+ v-model="tagName"
+ type="text"
+ class="form-control"
+ aria-describedby="tag-name-help"
+ disabled
+ />
+ <div id="tag-name-help" class="form-text text-muted">
+ {{ __('Choose an existing tag, or create a new one') }}
+ </div>
+ </gl-form-group>
+ </div>
+ <gl-form-group>
+ <label for="release-title">{{ __('Release title') }}</label>
+ <gl-form-input
+ id="release-title"
+ ref="releaseTitleInput"
+ v-model="releaseTitle"
+ v-autofocusonshow
+ autofocus
+ type="text"
+ class="form-control"
+ />
+ </gl-form-group>
+ <gl-form-group>
+ <label for="release-notes">{{ __('Release notes') }}</label>
+ <div class="bordered-box pr-3 pl-3">
+ <markdown-field
+ :can-attach-file="true"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :add-spacing-classes="false"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <textarea
+ id="release-notes"
+ slot="textarea"
+ v-model="releaseNotes"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="__('Release notes')"
+ :placeholder="__('Write your release notes or drag your files here…')"
+ @keydown.meta.enter="updateRelease()"
+ @keydown.ctrl.enter="updateRelease()"
+ >
+ </textarea>
+ </markdown-field>
+ </div>
+ </gl-form-group>
+
+ <div class="d-flex pt-3">
+ <gl-button
+ class="mr-auto js-submit-button"
+ variant="success"
+ type="submit"
+ :aria-label="__('Save changes')"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button
+ class="js-cancel-button"
+ variant="default"
+ type="button"
+ :aria-label="__('Cancel')"
+ @click="navigateToReleasesPage()"
+ >
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/detail/index.js b/app/assets/javascripts/releases/detail/index.js
new file mode 100644
index 00000000000..3da971e6d90
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import ReleaseDetailApp from './components/app.vue';
+import createStore from './store';
+
+export default () => {
+ const el = document.getElementById('js-edit-release-page');
+
+ const store = createStore(el.dataset);
+ store.dispatch('setInitialState', el.dataset);
+
+ return new Vue({
+ el,
+ store,
+ components: { ReleaseDetailApp },
+ render(createElement) {
+ return createElement('release-detail-app');
+ },
+ });
+};
diff --git a/app/assets/javascripts/releases/detail/store/actions.js b/app/assets/javascripts/releases/detail/store/actions.js
new file mode 100644
index 00000000000..c9749582f5c
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/actions.js
@@ -0,0 +1,62 @@
+import * as types from './mutation_types';
+import api from '~/api';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export const setInitialState = ({ commit }, initialState) =>
+ commit(types.SET_INITIAL_STATE, initialState);
+
+export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
+export const receiveReleaseSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_RELEASE_SUCCESS, data);
+export const receiveReleaseError = ({ commit }, error) => {
+ commit(types.RECEIVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while getting the release details'));
+};
+
+export const fetchRelease = ({ dispatch, state }) => {
+ dispatch('requestRelease');
+
+ return api
+ .release(state.projectId, state.tagName)
+ .then(({ data: release }) => {
+ const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true });
+ dispatch('receiveReleaseSuccess', camelCasedRelease);
+ })
+ .catch(error => {
+ dispatch('receiveReleaseError', error);
+ });
+};
+
+export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
+export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+
+export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
+export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => {
+ commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
+ dispatch('navigateToReleasesPage');
+};
+export const receiveUpdateReleaseError = ({ commit }, error) => {
+ commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while saving the release details'));
+};
+
+export const updateRelease = ({ dispatch, state }) => {
+ dispatch('requestUpdateRelease');
+
+ return api
+ .updateRelease(state.projectId, state.tagName, {
+ name: state.release.name,
+ description: state.release.description,
+ })
+ .then(() => dispatch('receiveUpdateReleaseSuccess'))
+ .catch(error => {
+ dispatch('receiveUpdateReleaseError', error);
+ });
+};
+
+export const navigateToReleasesPage = ({ state }) => {
+ redirectTo(state.releasesPagePath);
+};
diff --git a/app/assets/javascripts/releases/detail/store/index.js b/app/assets/javascripts/releases/detail/store/index.js
new file mode 100644
index 00000000000..e8623a49356
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state,
+ });
diff --git a/app/assets/javascripts/releases/detail/store/mutation_types.js b/app/assets/javascripts/releases/detail/store/mutation_types.js
new file mode 100644
index 00000000000..75e1d78a645
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/mutation_types.js
@@ -0,0 +1,12 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+export const REQUEST_RELEASE = 'REQUEST_RELEASE';
+export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
+export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
+
+export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
+export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
+
+export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
+export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
+export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
diff --git a/app/assets/javascripts/releases/detail/store/mutations.js b/app/assets/javascripts/releases/detail/store/mutations.js
new file mode 100644
index 00000000000..d739978d755
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/mutations.js
@@ -0,0 +1,42 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_STATE](state, initialState) {
+ Object.keys(state).forEach(key => {
+ state[key] = initialState[key];
+ });
+ },
+
+ [types.REQUEST_RELEASE](state) {
+ state.isFetchingRelease = true;
+ },
+ [types.RECEIVE_RELEASE_SUCCESS](state, data) {
+ state.fetchError = undefined;
+ state.isFetchingRelease = false;
+ state.release = data;
+ },
+ [types.RECEIVE_RELEASE_ERROR](state, error) {
+ state.fetchError = error;
+ state.isFetchingRelease = false;
+ state.release = undefined;
+ },
+
+ [types.UPDATE_RELEASE_TITLE](state, title) {
+ state.release.name = title;
+ },
+ [types.UPDATE_RELEASE_NOTES](state, notes) {
+ state.release.description = notes;
+ },
+
+ [types.REQUEST_UPDATE_RELEASE](state) {
+ state.isUpdatingRelease = true;
+ },
+ [types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) {
+ state.updateError = undefined;
+ state.isUpdatingRelease = false;
+ },
+ [types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) {
+ state.updateError = error;
+ state.isUpdatingRelease = false;
+ },
+};
diff --git a/app/assets/javascripts/releases/detail/store/state.js b/app/assets/javascripts/releases/detail/store/state.js
new file mode 100644
index 00000000000..ff98e2bed78
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+ projectId: null,
+ tagName: null,
+ releasesPagePath: null,
+ markdownDocsPath: null,
+ markdownPreviewPath: null,
+
+ release: null,
+
+ isFetchingRelease: false,
+ fetchError: null,
+
+ isUpdatingRelease: false,
+ updateError: null,
+});
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index c73db2668ec..ecd32dcd0ce 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -123,7 +123,7 @@ ul.content-list {
font-weight: $gl-font-weight-bold;
}
- a:not(.default-link-color) {
+ a {
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
new file mode 100644
index 00000000000..a6d30522ff7
--- /dev/null
+++ b/app/assets/stylesheets/pages/tags.scss
@@ -0,0 +1,3 @@
+.tag-release-link {
+ color: $blue-600 !important;
+}
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index d88ec06a18b..efd5f0fc607 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -4,18 +4,31 @@ class HealthController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
include RequiresWhitelistedMonitoringClient
+ CHECKS = [
+ Gitlab::HealthChecks::DbCheck,
+ Gitlab::HealthChecks::Redis::RedisCheck,
+ Gitlab::HealthChecks::Redis::CacheCheck,
+ Gitlab::HealthChecks::Redis::QueuesCheck,
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
+ ].freeze
+
def readiness
- render_probe(::Gitlab::HealthChecks::Probes::Readiness)
+ # readiness check is a collection with all above application-level checks
+ render_checks(*CHECKS)
end
def liveness
- render_probe(::Gitlab::HealthChecks::Probes::Liveness)
+ # liveness check is a collection without additional checks
+ render_checks
end
private
- def render_probe(probe_class)
- result = probe_class.new.execute
+ def render_checks(*checks)
+ result = Gitlab::HealthChecks::Probes::Collection
+ .new(*checks)
+ .execute
# disable static error pages at the gitlab-workhorse level, we want to see this error response even in production
headers["X-GitLab-Custom-Error"] = 1 unless result.success?
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 32111b07a0b..766e2f86ea2 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController
@deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
end
- # rubocop: disable CodeReuse/ActiveRecord
def deployment
- @deployment ||= environment.deployments.find_by(iid: params[:id])
+ @deployment ||= environment.deployments.find_successful_deployment!(params[:id])
end
- # rubocop: enable CodeReuse/ActiveRecord
def environment
@environment ||= project.environments.find(params[:environment_id])
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 1913d7cd580..4a37dfe5c19 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -51,9 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
- note_positions = renderable_notes.map(&:position).compact
- @diffs.unfold_diff_files(note_positions)
-
+ @diffs.unfold_diff_files(note_positions.unfoldable)
@diffs.write_cache
request = {
@@ -140,6 +138,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
end
+ def note_positions
+ @note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
+ end
+
def renderable_notes
define_diff_comment_vars unless @notes
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 42fe42398f1..3c70ff3b59f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -289,7 +289,8 @@ module ApplicationSettingsHelper
:snowplow_collector_hostname,
:snowplow_cookie_domain,
:snowplow_enabled,
- :snowplow_site_id
+ :snowplow_site_id,
+ :push_event_hooks_limit
]
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 2b7320817ed..52f189b122f 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -18,12 +18,16 @@ module EnvironmentHelper
end
end
+ def deployment_path(deployment)
+ [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ end
+
def deployment_link(deployment, text: nil)
return unless deployment
link_label = text ? text : "##{deployment.iid}"
- link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ link_to link_label, deployment_path(deployment)
end
def last_deployment_link_for_environment_build(project, build)
@@ -32,4 +36,31 @@ module EnvironmentHelper
deployment_link(environment.last_deployment)
end
+
+ def render_deployment_status(deployment)
+ status = deployment.status
+
+ status_text =
+ case status
+ when 'created'
+ s_('Deployment|created')
+ when 'running'
+ s_('Deployment|running')
+ when 'success'
+ s_('Deployment|success')
+ when 'failed'
+ s_('Deployment|failed')
+ when 'canceled'
+ s_('Deployment|canceled')
+ end
+
+ klass = "ci-status ci-#{status.dasherize}"
+ text = "#{ci_icon_for_status(status)} #{status_text}".html_safe
+
+ if deployment.deployable
+ link_to(text, deployment_path(deployment), class: klass)
+ else
+ content_tag(:span, text, class: klass)
+ end
+ end
end
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 3186bbd9322..68a19152d8f 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -19,4 +19,14 @@ module ReleasesHelper
documentation_path: help_page
}
end
+
+ def data_for_edit_release_page
+ {
+ project_id: @project.id,
+ tag_name: @release.tag,
+ markdown_preview_path: preview_markdown_path(@project),
+ markdown_docs_path: help_page_path('user/markdown'),
+ releases_page_path: project_releases_path(@project, anchor: @release.tag)
+ }
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 02f214341fb..0724ee8f39d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -214,6 +214,9 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
+ validates :push_event_hooks_limit,
+ numericality: { greater_than_or_equal_to: 0 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e2579316fdd..e9aab4a3d05 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -82,6 +82,7 @@ module ApplicationSettingImplementation
polling_interval_multiplier: 1,
project_export_enabled: true,
protected_ci_variables: false,
+ push_event_hooks_limit: 3,
raw_blob_request_limit: 300,
recaptcha_enabled: false,
login_recaptcha_protection_enabled: false,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 30694313f7a..7ccd5e98360 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user
- belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) do
Deployment.where(project: s.project).maximum(:iid) if s&.project
@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
scope :for_environment, -> (environment) { where(environment_id: environment) }
+ scope :visible, -> { where(status: %i[running success failed canceled]) }
+
state_machine :status, initial: :created do
event :run do
transition created: :running
@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord
find(ids)
end
+ def self.find_successful_deployment!(iid)
+ success.find_by!(iid: iid)
+ end
+
def commit
project.commit(sha)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index fe438b142b2..af0c219d9a0 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,8 @@ class Environment < ApplicationRecord
belongs_to :project, required: true
- has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
@@ -81,6 +82,10 @@ class Environment < ApplicationRecord
pluck(:name)
end
+ def self.find_or_create_by_name(name)
+ find_or_create_by(name: name)
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
diff --git a/app/models/project.rb b/app/models/project.rb
index 4d518862146..f1e232e95f8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -281,7 +281,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
- has_many :deployments, -> { success }
+ has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
index d4f2f3c52b1..1a92b735e36 100644
--- a/app/policies/deployment_policy.rb
+++ b/app/policies/deployment_policy.rb
@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
can?(:update_build, @subject.deployable)
end
- rule { ~can_retry_deployable }.policy do
+ condition(:has_deployable) do
+ @subject.deployable.present?
+ end
+
+ condition(:can_update_deployment) do
+ can?(:update_deployment, @subject.environment)
+ end
+
+ rule { has_deployable & ~can_retry_deployable }.policy do
prevent :create_deployment
prevent :update_deployment
end
+
+ rule { ~can_update_deployment }.policy do
+ prevent :update_deployment
+ end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a3540f31077..ea2be37d7e6 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image
enable :create_environment
enable :create_deployment
+ enable :update_deployment
enable :create_release
enable :update_release
end
diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb
new file mode 100644
index 00000000000..2572802e6a1
--- /dev/null
+++ b/app/services/deployments/after_create_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Deployments
+ class AfterCreateService
+ attr_reader :deployment
+ attr_reader :deployable
+
+ delegate :environment, to: :deployment
+ delegate :variables, to: :deployable
+ delegate :options, to: :deployable, allow_nil: true
+
+ def initialize(deployment)
+ @deployment = deployment
+ @deployable = deployment.deployable
+ end
+
+ def execute
+ deployment.create_ref
+ deployment.invalidate_cache
+
+ update_environment(deployment)
+
+ deployment
+ end
+
+ def update_environment(deployment)
+ ActiveRecord::Base.transaction do
+ if (url = expanded_environment_url)
+ environment.external_url = url
+ end
+
+ environment.fire_state_event(action)
+
+ if environment.save && !environment.stopped?
+ deployment.update_merge_request_metrics!
+ end
+ end
+ end
+
+ private
+
+ def environment_options
+ options&.dig(:environment) || {}
+ end
+
+ def expanded_environment_url
+ ExpandVariables.expand(environment_url, -> { variables }) if environment_url
+ end
+
+ def environment_url
+ environment_options[:url]
+ end
+
+ def action
+ environment_options[:action] || 'start'
+ end
+ end
+end
+
+Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb
new file mode 100644
index 00000000000..89e3f7c8b83
--- /dev/null
+++ b/app/services/deployments/create_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Deployments
+ class CreateService
+ attr_reader :environment, :current_user, :params
+
+ def initialize(environment, current_user, params)
+ @environment = environment
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ create_deployment.tap do |deployment|
+ AfterCreateService.new(deployment).execute if deployment.persisted?
+ end
+ end
+
+ def create_deployment
+ environment.deployments.create(deployment_attributes)
+ end
+
+ def deployment_attributes
+ # We use explicit parameters here so we never by accident allow parameters
+ # to be set that one should not be able to set (e.g. the row ID).
+ {
+ cluster_id: environment.deployment_platform&.cluster_id,
+ project_id: environment.project_id,
+ environment_id: environment.id,
+ ref: params[:ref],
+ tag: params[:tag],
+ sha: params[:sha],
+ user: current_user,
+ on_stop: params[:on_stop],
+ status: params[:status]
+ }
+ end
+ end
+end
diff --git a/app/services/deployments/update_service.rb b/app/services/deployments/update_service.rb
new file mode 100644
index 00000000000..7c8215d28f2
--- /dev/null
+++ b/app/services/deployments/update_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Deployments
+ class UpdateService
+ attr_reader :deployment, :params
+
+ def initialize(deployment, params)
+ @deployment = deployment
+ @params = params
+ end
+
+ def execute
+ deployment.update(status: params[:status])
+ end
+ end
+end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 97047d96de1..b1faef58e33 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -62,6 +62,8 @@ module Git
end
def execute_project_hooks
+ return unless params.fetch(:execute_project_hooks, true)
+
# Creating push_data invokes one CommitDelta RPC per commit. Only
# build this data if we actually need it.
project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index 33925147750..62159d4e7e5 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -17,7 +17,7 @@ module Git
changes_by_action = group_changes_by_action(changes)
changes_by_action.each do |_, changes|
- process_changes(ref_type, changes) if changes.any?
+ process_changes(ref_type, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
end
end
@@ -34,7 +34,11 @@ module Git
:pushed
end
- def process_changes(ref_type, changes)
+ def execute_project_hooks?(changes)
+ (changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project)
+ end
+
+ def process_changes(ref_type, changes, execute_project_hooks:)
push_service_class = push_service_class_for(ref_type)
changes.each do |change|
@@ -43,7 +47,8 @@ module Git
current_user,
change: change,
push_options: params[:push_options],
- create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
+ create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
+ execute_project_hooks: execute_project_hooks
).execute
end
end
diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb
deleted file mode 100644
index 730210c611a..00000000000
--- a/app/services/update_deployment_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateDeploymentService
- attr_reader :deployment
- attr_reader :deployable
-
- delegate :environment, to: :deployment
- delegate :variables, to: :deployable
-
- def initialize(deployment)
- @deployment = deployment
- @deployable = deployment.deployable
- end
-
- def execute
- deployment.create_ref
- deployment.invalidate_cache
-
- ActiveRecord::Base.transaction do
- environment.external_url = expanded_environment_url if
- expanded_environment_url
-
- environment.fire_state_event(action)
-
- break unless environment.save
- break if environment.stopped?
-
- deployment.tap(&:update_merge_request_metrics!)
- end
-
- deployment
- end
-
- private
-
- def environment_options
- @environment_options ||= deployable.options&.dig(:environment) || {}
- end
-
- def expanded_environment_url
- return @expanded_environment_url if defined?(@expanded_environment_url)
- return unless environment_url
-
- @expanded_environment_url =
- ExpandVariables.expand(environment_url, -> { variables })
- end
-
- def environment_url
- environment_options[:url]
- end
-
- def action
- environment_options[:action] || 'start'
- end
-end
-
-UpdateDeploymentService.prepend_if_ee('EE::UpdateDeploymentService')
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index b52171afc69..22458223b93 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -20,5 +20,10 @@
= f.number_field :raw_blob_request_limit, class: 'form-control'
.form-text.text-muted
= _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.')
+ .form-group
+ = f.label :push_event_hooks_limit, class: 'label-bold'
+ = f.number_field :push_event_hooks_limit, class: 'form-control'
+ .form-text.text-muted
+ = _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 688b8f001c3..7c73bbc7479 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title "Repository"
- page_title @blob.path, @ref
-- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit)
+- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
.js-signature-container{ data: { 'signatures-path': signatures_path } }
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index ef2ab4c698e..8270477ed3f 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,31 +1,49 @@
.gl-responsive-table-row.deployment{ role: 'row' }
+ .table-section.section-15{ role: 'gridcell' }
+ .table-mobile-header{ role: 'rowheader' }= _("Status")
+ .table-mobile-content
+ = render_deployment_status(deployment)
+
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid}
- .table-section.section-30{ role: 'gridcell' }
+ .table-section.section-10{ role: 'gridcell' }
+ .table-mobile-header{ role: 'rowheader' }= _("Triggerer")
+ .table-mobile-content
+ - if deployment.deployed_by
+ = user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
+
+ .table-section.section-25{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment
- .table-section.section-25.build-column{ role: 'gridcell' }
+ .table-section.section-10.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
.flex-truncate-child
- = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
+ = link_to deployment_path(deployment), class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id})
- - if deployment.deployed_by
- %div
- by
- = user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
+ - else
+ .badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
+ = s_('Deployment|API')
- .table-section.section-15{ role: 'gridcell' }
+ .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created")
+ %span.table-mobile-content.flex-truncate-parent
+ %span.flex-truncate-child
+ = time_ago_with_tooltip(deployment.created_at)
+
+ .table-section.section-10{ role: 'gridcell' }
+ .table-mobile-header{ role: 'rowheader' }= _("Deployed")
- if deployment.deployed_at
- %span.table-mobile-content= time_ago_with_tooltip(deployment.deployed_at)
+ %span.table-mobile-content.flex-truncate-parent
+ %span.flex-truncate-child
+ = time_ago_with_tooltip(deployment.deployed_at)
- .table-section.section-20.table-button-footer{ role: 'gridcell' }
+ .table-section.section-10.table-button-footer{ role: 'gridcell' }
.btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index d6bf8d564de..dffa5e4ba40 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, deployment)
+- if deployment.deployable && can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
- if deployment.last?
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 75da151f329..c4c39c227c6 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -60,10 +60,13 @@
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
+ .table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
- .table-section.section-30{ role: 'columnheader' }= _('Commit')
- .table-section.section-25{ role: 'columnheader' }= _('Job')
- .table-section.section-15{ role: 'columnheader' }= _('Created')
+ .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
+ .table-section.section-25{ role: 'columnheader' }= _('Commit')
+ .table-section.section-10{ role: 'columnheader' }= _('Job')
+ .table-section.section-10{ role: 'columnheader' }= _('Created')
+ .table-section.section-10{ role: 'columnheader' }= _('Deployed')
= render @deployments
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
index acc2c50294f..fe89d2fb748 100644
--- a/app/views/projects/issues/import_csv/_button.html.haml
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -3,7 +3,7 @@
%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
data: { toggle: 'modal', target: '.issues-import-modal' } }
- if type == :icon
- = sprite_icon('upload')
+ = sprite_icon('import')
- else
= _('Import CSV')
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
new file mode 100644
index 00000000000..88ca64f2af0
--- /dev/null
+++ b/app/views/projects/releases/edit.html.haml
@@ -0,0 +1,3 @@
+- page_title _('Edit Release')
+
+#js-edit-release-page{ data: data_for_edit_release_page }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 3f6cd628d64..c7bd0262c54 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -24,7 +24,7 @@
.text-secondary
= icon('rocket')
= _("Release")
- = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'default-link-color'
+ = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'tag-release-link'
- if release.description.present?
.description.md.prepend-top-default
= markdown_field(release, :description)
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index 83f60fa6fe2..4fed95e2607 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,4 +1,4 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
- = icon('rss')
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
+ = sprite_icon('rss')
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
- = custom_icon('icon_calendar')
+ = sprite_icon('calendar')
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
index da517f3fb26..3c7e384365a 100644
--- a/app/workers/deployments/success_worker.rb
+++ b/app/workers/deployments/success_worker.rb
@@ -10,7 +10,7 @@ module Deployments
Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success?
- UpdateDeploymentService.new(deployment).execute
+ Deployments::AfterCreateService.new(deployment).execute
end
end
end