diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-16 18:08:01 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-16 18:08:01 +0000 |
commit | 8e45d25f7dde6508839ffee719c0ddc2cf6b12d3 (patch) | |
tree | 9839e7fe63b36904d40995ebf519124c9a8f7681 /app | |
parent | 00c78fb814d7ce00989ac04edd6cdaa3239da284 (diff) | |
download | gitlab-ce-8e45d25f7dde6508839ffee719c0ddc2cf6b12d3.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |