Commit 8e45d25f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 00c78fb8
VERSION merge=ours
Dangerfile gitlab-language=ruby
db/schema.rb merge=merge_db_schema
......@@ -67,3 +67,18 @@ docs lint:
- bundle exec nanoc check internal_links
# Check the internal anchor links
- bundle exec nanoc check internal_anchors
graphql-docs-verify:
extends:
- .default-tags
- .default-retry
- .default-cache
- .default-only
- .default-before_script
- .only-graphql-changes
variables:
SETUP_DB: "false"
stage: test
needs: ["setup-test-env"]
script:
- bundle exec rake gitlab:graphql:check_docs
......@@ -53,7 +53,7 @@
- gitlab-org
- docker
gitlab:assets:compile:
gitlab:assets:compile pull-push-cache:
extends: .gitlab:assets:compile-metadata
only:
refs:
......@@ -63,9 +63,6 @@ gitlab:assets:compile:
gitlab:assets:compile pull-cache:
extends: .gitlab:assets:compile-metadata
except:
refs:
- master
cache:
policy: pull
......@@ -89,14 +86,14 @@ gitlab:assets:compile pull-cache:
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
cache:
key: "assets-compile:test:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v6"
key: "assets-compile:v7"
artifacts:
expire_in: 7d
paths:
- node_modules
- public/assets
compile-assets:
compile-assets pull-push-cache:
extends: .compile-assets-metadata
only:
refs:
......@@ -104,13 +101,25 @@ compile-assets:
cache:
policy: pull-push
compile-assets pull-cache:
extends: .compile-assets-metadata
except:
compile-assets pull-push-cache foss:
extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
only:
refs:
- master
cache:
policy: pull-push
key: "assets-compile:v7:foss"
compile-assets pull-cache:
extends: .compile-assets-metadata
cache:
policy: pull
compile-assets pull-cache foss:
extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
cache:
policy: pull
key: "assets-compile:v7:foss"
.only-code-frontend-job-base:
extends:
......@@ -121,7 +130,9 @@ compile-assets pull-cache:
- .default-before_script
- .only-code-changes
- .use-pg9
dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
dependencies: ["setup-test-env", "compile-assets pull-cache"]
.karma-base:
extends: .only-code-frontend-job-base
......@@ -195,6 +206,7 @@ jest-foss:
- .default-cache
- .default-only
- .only-code-changes
stage: test
dependencies: []
cache:
key: "$CI_JOB_NAME"
......@@ -227,7 +239,9 @@ webpack-dev-server:
- .default-cache
- .default-only
- .only-code-changes
dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables:
WEBPACK_MEMORY_TEST: "true"
script:
......
......@@ -71,6 +71,12 @@
- "doc/**/*"
- ".markdownlint.json"
.only-graphql-changes:
only:
changes:
- "{,ee/}app/graphql/**/*"
- "{,ee/}lib/gitlab/graphql/**/*"
.only-code-qa-changes:
only:
changes:
......@@ -153,4 +159,4 @@
.only-ee-as-if-foss:
extends: .only-ee
variables:
IS_GITLAB_EE: '0'
FOSS_ONLY: '1'
......@@ -11,7 +11,7 @@ pages:
variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
stage: pages
dependencies: ["coverage", "karma", "gitlab:assets:compile"]
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
script:
- mv public/ .public/
- mkdir public/
......
......@@ -71,4 +71,4 @@ schedule:package-and-qa:
- .package-and-qa-base
- .only-code-qa-changes
- .only-canonical-schedules
needs: ["build-qa-image", "gitlab:assets:compile"]
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
......@@ -53,6 +53,8 @@ setup-test-env:
.rspec-base:
extends: .only-code-rails-job-base
stage: test
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
dependencies: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
script:
- source scripts/rspec_helpers.sh
- rspec_paralellized_job "--tag ~quarantine --tag ~geo"
......@@ -69,6 +71,11 @@ setup-test-env:
reports:
junit: junit_rspec.xml
.rspec-base-foss:
extends: [".rspec-base", ".only-ee-as-if-foss"]
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache foss"]
dependencies: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache foss"]
.rspec-base-pg9:
extends:
- .rspec-base
......@@ -76,9 +83,8 @@ setup-test-env:
.rspec-base-pg9-foss:
extends:
- .rspec-base
- .rspec-base-foss
- .use-pg9
- .only-ee-as-if-foss
.rspec-base-pg10:
extends:
......@@ -106,10 +112,9 @@ rspec system pg9:
extends: .rspec-base-pg9
parallel: 24
# TODO: This requires FOSS assets
# rspec system pg9-foss:
# extends: .rspec-base-pg9-foss
# parallel: 24
rspec system pg9-foss:
extends: .rspec-base-pg9-foss
parallel: 24
rspec unit pg10:
extends: .rspec-base-pg10
......@@ -229,7 +234,9 @@ rspec fast_spec_helper:
static-analysis:
extends: .only-code-qa-rails-job-base
dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables:
SETUP_DB: "false"
script:
......@@ -252,16 +259,16 @@ downtime_check:
variables:
- $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
dependencies: ["setup-test-env"]
.db-job-base:
extends:
- .only-code-rails-job-base
- .use-pg9
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
dependencies: ["setup-test-env"]
# DB migration, rollback, and seed jobs
db:migrate:reset:
......
......@@ -81,7 +81,7 @@ schedule:review-build-cng:
extends:
- .review-build-cng-base
- .only-review-schedules
needs: ["gitlab:assets:compile"]
needs: ["gitlab:assets:compile pull-cache"]
.review-deploy-base:
extends:
......@@ -97,7 +97,7 @@ schedule:review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master"
GITLAB_HELM_CHART_REF: "v2.3.7"
GITLAB_EDITION: "ce"
environment:
name: review/${CI_COMMIT_REF_NAME}
......
......@@ -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);
......
import ZenMode from '~/zen_mode';
import initEditRelease from '~/releases/detail';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
initEditRelease();
});
<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>
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');
},
});
};
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);
};
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,
});
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';
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;
},
};
export default () => ({
projectId: null,
tagName: null,
releasesPagePath: null,
markdownDocsPath: null,
markdownPreviewPath: null,
release: null,
isFetchingRelease: false,
fetchError: null,
isUpdatingRelease: false,
updateError: null,
});
......@@ -123,7 +123,7 @@ ul.content-list {
font-weight: $gl-font-weight-bold;
}
a:not(.default-link-color) {
a {
color: $gl-text-color;
}
......
.tag-release-link {
color: $blue-600 !important;
}
......@@ -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?
......
......@@ -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])
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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,
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......@@ -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
......
# 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')
# 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
# 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
......@@ -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)
......
......@@ -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
......
# 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')
......@@ -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"
- 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 } }
......
.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
- 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?
......
......@@ -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
......
......@@ -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')
- page_title _('Edit Release')
#js-edit-release-page{ data: data_for_edit_release_page }
......@@ -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)
......
= 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')
......@@ -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
......
---
title: Fix showing diff when it has legacy diff notes
merge_request: 18510
author:
type: fixed
---
title: Don't execute webhooks/services when above limit
merge_request: 17874
author:
type: performance
---
title: Use correct icons for issue actions
merge_request:
author:
type: other
---
title: Introduce new Ansi2json parser to convert job logs to JSON
merge_request: 18133
author:
type: added
---
title: Add individual inherited member lookup API
merge_request: 17744
author:
type: added
---
title: Add API for manually creating and updating deployments
merge_request: 17620
author:
type: added
---
title: Add "Edit Release" page
merge_request: 18033
author:
type: added
---
title: Fix button link foreground color
merge_request: 18669
author:
type: fixed
......@@ -3,12 +3,12 @@ const path = require('path');
const ROOT_PATH = path.resolve(__dirname, '../..');
// The `IS_GITLAB_EE` is always `string` or `nil`
// The `FOSS_ONLY` is always `string` or `nil`
// Thus the nil or empty string will result
// in using default value: true
// in using default value: false
//
// The behavior needs to be synchronised with
// lib/gitlab.rb: Gitlab.ee?
const isFossOnly = JSON.parse(process.env.FOSS_ONLY || 'false');
module.exports =
fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) &&
(!process.env.IS_GITLAB_EE || JSON.parse(process.env.IS_GITLAB_EE));
fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) && !isFossOnly;
......@@ -380,7 +380,7 @@ module.exports = {
new webpack.DefinePlugin({
// This one is used to define window.gon.ee and other things properly in tests:
'process.env.IS_GITLAB_EE': JSON.stringify(IS_EE),
'process.env.IS_EE': JSON.stringify(IS_EE),
// This one is used to check against "EE" properly in application code
IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
}),
......
# frozen_string_literal: true
class AddPushEventHooksLimitToApplicationSettings < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:application_settings, :push_event_hooks_limit, :integer, default: 3)
end
def down
remove_column(:application_settings, :push_event_hooks_limit)
end
end
......@@ -338,6 +338,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", default: 3600
t.integer "push_event_hooks_limit", default: 3, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
......
......@@ -217,14 +217,19 @@ workload. Your workload is influenced by factors such as - but not limited to -
how active your users are, how much automation you use, mirroring, and
repo/change size.
- 3 PostgreSQL - 4 CPU, 16GiB memory per node
- 1 PgBouncer - 2 CPU, 4GiB memory
- 2 Redis - 2 CPU, 8GiB memory per node
- 3 Consul/Sentinel - 2 CPU, 2GiB memory per node
- 4 Sidekiq - 4 CPU, 16GiB memory per node
- 5 GitLab application nodes - 16 CPU, 64GiB memory per node
- 1 Gitaly - 16 CPU, 64GiB memory
- 1 Monitoring node - 2 CPU, 8GiB memory, 100GiB local storage
| Service | Configuration | GCP type |
| ------------------------------|-------------------------|----------------|
| 3 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
| 3 PostgreSQL | 4 vCPU, 15GB Memory | n1-standard-4 |
| 1 PgBouncer | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| X Gitaly[^1] <br> - Gitaly Ruby workers on each node set to 90% of available CPUs with 16 threads | 16 vCPU, 60GB Memory | n1-standard-16 |
| 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
### 25,000 User Configuration
......@@ -249,7 +254,7 @@ adjusted prior to certification based on performance testing.
| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| 1 NFS Server | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
......@@ -277,15 +282,15 @@ testing.
| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| 1 NFS Server | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
[^1]: Gitaly node requirements are dependent on customer data. We recommend 2
nodes as an absolute minimum for performance at the 25,000 user scale and
4 nodes as an absolute minimum at the 50,000 user scale, but additional
nodes should be considered in conjunction with a review of project counts
and sizes.
nodes as an absolute minimum for performance at the 10,000 and 25,000 user
scale and 4 nodes as an absolute minimum at the 50,000 user scale, but
additional nodes should be considered in conjunction with a review of
project counts and sizes.
[^2]: HAProxy is the only tested and recommended load balancer. Additional
options may be supported in the future.
......@@ -223,3 +223,100 @@ Example of response
}
}
```
## Create a deployment
```
POST /projects/:id/deployments
```
| Attribute | Type | Required | Description |
|------------------|----------------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `environment` | string | yes | The name of the environment to create the deployment for |
| `sha` | string | yes | The SHA of the commit that is deployed |
| `ref` | string | yes | The name of the branch or tag that is deployed |
| `tag` | boolean | yes | A boolean that indicates if the deployed ref is a tag (true) or not (false) |
| `status` | string | yes | The status of the deployment |
The status can be one of the following values:
- created
- running
- success
- failed
- canceled
```bash
curl --data "environment=production&sha=a91957a858320c0e17f3a0eca7cfacbff50ea29a&ref=master&tag=false&status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments"
```
Example of a response:
```json
{
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"status": "success",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com"
},
"deployable": null
}
```
## Updating a deployment
```
PUT /projects/:id/deployments/:deployment_id
```
| Attribute | Type | Required | Description |
|------------------|----------------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `deployment_id` | integer | yes | The ID of the deployment to update |
| `status` | string | yes | The new status of the deployment |
```bash
curl --request PUT --data "status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42"
```
Example of a response:
```json
{
"id": 42,
"iid": 2,
"ref": "master",
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"created_at": "2016-08-11T11:32:35.444Z",
"status": "success",
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"environment": {
"id": 9,
"name": "production",
"external_url": "https://about.gitlab.com"
},
"deployable": null
}
```
This diff is collapsed.
......@@ -26,6 +26,7 @@ GET /projects/:id/members
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `query` | string | no | A query string to search for members |
| `user_ids` | array of integers | no | Filter the results on the given user IDs |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members
......@@ -62,9 +63,8 @@ Example response:
## List all members of a group or project including inherited members
Gets a list of group or project members viewable by the authenticated user, including inherited members through ancestor groups.
When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project access_level (if exists)
or the access_level for the user in the first group which he belongs to in the project groups ancestors chain.
**Note:** We plan to [change](https://gitlab.com/gitlab-org/gitlab-foss/issues/62284) this behavior to return highest access_level instead.
When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project `access_level` (if exists)
or the `access_level` for the user in the first group which he belongs to in the project groups ancestors chain.
```
GET /groups/:id/members/all
......@@ -75,6 +75,7 @@ GET /projects/:id/members/all
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `query` | string | no | A query string to search for members |
| `user_ids` | array of integers | no | Filter the results on the given user IDs |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all
......@@ -120,7 +121,7 @@ Example response:
## Get a member of a group or project
Gets a member of a group or project.
Gets a member of a group or project. Returns only direct members and not inherited members through ancestor groups.
```
GET /groups/:id/members/:user_id
......@@ -152,6 +153,42 @@ Example response:
}
```
## Get a member of a group or project, including inherited members
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/17744) in GitLab 12.4.
Gets a member of a group or project, including members inherited through ancestor groups. See the corresponding [endpoint to list all inherited members](#list-all-members-of-a-group-or-project-including-inherited-members) for details.
```
GET /groups/:id/members/all/:user_id
GET /projects/:id/members/all/:user_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `user_id` | integer | yes | The user ID of the member |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all/:user_id
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/members/all/:user_id
```
Example response:
```json
{
"id": 1,
"username": "raymond_smith",
"name": "Raymond Smith",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
"access_level": 30,
"expires_at": null
}
```
## Add a member to a group or project
Adds a member to a group or project.
......
......@@ -289,6 +289,7 @@ are listed in the descriptions of the relevant settings.
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. |
| `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.
| `push_event_hooks_limit` | integer | no | 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. |
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. |
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. |
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |
......
......@@ -14,7 +14,7 @@ tasks such as:
To request access to Chatops on GitLab.com:
1. Log into <https://ops.gitlab.net/users/sign_in> **using the same username** as for GitLab.com (you may have to rename it).
1. Ask [anyone in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
1. Ask [an owner/maintainer in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members?search=&sort=access_level_desc) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
## See also
......
......@@ -492,19 +492,50 @@ For other punctuation rules, please refer to the
- Use inline link markdown markup `[Text](https://example.com)`.
It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
- To link to internal documentation, use relative links, not full URLs. Use `../` to
navigate to high-level directories, and always add the file name `file.md` at the
end of the link with the `.md` extension, not `.html`.
Example: instead of `[text](../../merge_requests/)`, use
`[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or,
for anchor links, `[text](../../ci/README.md#examples)`.
Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
section of GitLab.
- To link from CE to EE-only documentation, use the EE-only doc full URL.
- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/).
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](LINK)`.
### Links to internal documentation
- To link to internal documentation, use relative links, not full URLs.
Use `../` to navigate to high-level directories. Links should not refer to root.
Don't:
```md
[Geo Troubleshooting](https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html)
[Geo Troubleshooting](/ee/administration/geo/replication/troubleshooting.md)
```
Do:
```md
[Geo Troubleshooting](../../geo/replication/troubleshooting.md)
```
- Always add the file name `file.md` at the end of the link with the `.md` extension, not `.html`.
Don't:
```md
[merge requests](../../merge_requests/)
[issues](../../issues/tags.html)
[issue tags](../../issues/tags.html#stages)
```
Do:
```md
[merge requests](../../merge_requests/index.md)
[issues](../../issues/tags.md)
[issue tags](../../issues/tags.md#stages)
```
- Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
section of GitLab.
### Links requiring permissions
Don't link directly to:
......
......@@ -20,9 +20,9 @@ should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
You can force GitLab to act as CE by either deleting the `ee/` directory or by
setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
to something that evaluates as `false`. The same works for running tests
(for example `IS_GITLAB_EE=0 yarn jest`).
setting the [`FOSS_ONLY` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
to something that evaluates as `true`. The same works for running tests
(for example `FOSS_ONLY=1 yarn jest`).
[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab/issues/2500
......
......@@ -102,7 +102,7 @@ These common definitions are:
`docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services.
- `.only-ee`: Only creates a job for the `gitlab` project.
- `.only-ee-as-if-foss`: Same as `.only-ee` but simulate the FOSS project by
setting the `IS_GITLAB_EE='0'` environment variable.
setting the `FOSS_ONLY='1'` environment variable.
## Changes detection
......@@ -115,6 +115,7 @@ from a commit or MR by extending from the following CI definitions:
- `.only-qa-changes`: Allows a job to only be created upon QA-related changes.
- `.only-docs-changes`: Allows a job to only be created upon docs-related changes.
- `.only-code-qa-changes`: Allows a job to only be created upon code-related or QA-related changes.
- `.only-graphql-changes`: Allows a job to only be created upon graphql-related changes.
**See <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml>
for the list of exact patterns.**
......@@ -127,7 +128,7 @@ execute jobs out of order for the following jobs:
```mermaid
graph RL;
A[setup-test-env];
B["gitlab:assets:compile<br/>(master only)"];
B["gitlab:assets:compile pull-push-cache<br/>(master only)"];
C[gitlab:assets:compile pull-cache];
D["cache gems<br/>(master and tags only)"];
E[review-build-cng];
......@@ -136,7 +137,7 @@ graph RL;
G2["schedule:review-deploy<br/>(master only)"];
H[karma];
I[jest];
J["compile-assets<br/>(master only)"];
J["compile-assets pull-push-cache<br/>(master only)"];
K[compile-assets pull-cache];
L[webpack-dev-server];
M[coverage];
......@@ -145,39 +146,42 @@ graph RL;
P["schedule:package-and-qa<br/>(master schedule only)"];
Q[package-and-qa];
R[package-and-qa-manual];
S["RSpec<br/>(e.g. rspec unit pg9)"]
T[retrieve-tests-metadata];
subgraph "`prepare` stage"
A
F
J
K
J
T
end
subgraph "`test` stage"
B --> |needs| A;
C --> |needs| A;
D --> |needs| A;
H -.-> |depends on| A;
H -.-> |depends on| J;
H -.-> |depends on| K;
I -.-> |depends on| A;
I -.-> |depends on| J;
I -.-> |depends on| K;
L -.-> |depends on| A;
L -.-> |depends on| J;
L -.-> |depends on| K;
H -.-> |needs and depends on| A;
H -.-> |needs and depends on| K;
I -.-> |needs and depends on| A;
I -.-> |needs and depends on| K;
L -.-> |needs and depends on| A;
L -.-> |needs and depends on| K;
O -.-> |needs and depends on| A;
O -.-> |needs and depends on| K;
S -.-> |needs and depends on| A;
S -.-> |needs and depends on| K;
S -.-> |needs and depends on| T;
downtime_check --> |needs and depends on| A;
db:* --> |needs| A;
gitlab:setup --> |needs| A;
O -.-> |depends on| A;
O -.-> |depends on| B;
O -.-> |depends on| C;
downtime_check --> |needs and depends on| A;
graphql-docs-verify --> |needs| A;
end
subgraph "`review-prepare` stage"
E --> |needs| C;
X["schedule:review-build-cng<br/>(master schedule only)"] --> |needs| B;
X["schedule:review-build-cng<br/>(master schedule only)"] --> |needs| C;
end
subgraph "`review` stage"
......@@ -190,7 +194,7 @@ subgraph "`qa` stage"
Q --> |needs| F;
R --> |needs| C;
R --> |needs| F;
P --> |needs| B;
P --> |needs| C;
P --> |needs| F;
review-qa-smoke -.-> |needs and depends on| G;
review-qa-all -.-> |needs and depends on| G;
......@@ -209,7 +213,7 @@ subgraph "`post-test` stage"
end
subgraph "`pages` stage"
N -.-> |depends on| B;
N -.-> |depends on| C;
N -.-> |depends on| H;
N -.-> |depends on| M;
end
......
......@@ -10,13 +10,13 @@ Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
milestones.
![epics list view](img/epics_list_view.png)
![epics list view](img/epics_list_view_v12.3.png)
## Use cases
- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
- Track when the work for the group of issues is targeted to begin, and when it is targeted to end.
- Discuss and collaborate on feature ideas and scope at a high-level.
- Discuss and collaborate on feature ideas and scope at a high level.
## Creating an epic
......@@ -24,78 +24,114 @@ A paginated list of epics is available in each group from where you can create
a new epic. The list of epics includes also epics from all subgroups of the
selected group. From your group page:
1. Go to **Epics**
1. Click the **New epic** button at the top right
1. Enter a descriptive title and hit **Create epic**
1. Go to **Epics**.
1. Click **New epic**.
1. Enter a descriptive title and click **Create epic**.
Once created, you will be taken to the view for that newly-created epic where
you can change its title, description, start date, and due date.
You will be taken to the new epic where can edit the following details:
![epic view](img/epic_view.png)
- Title
- Description
- Start date
- Due date
- Labels
An epic's page contains the following tabs:
- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
- Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
- **Roadmap**: a roadmap view of child epics which have start and due dates.
![epic view](img/epic_view_v12.3.png)
## Adding an issue to an epic
Any issue that belongs to a project in the epic's group, or any of the epic's
subgroups, are eligible to be added. New issues appear at the top of the list of issues in the **Epics and Issues** tab.
An epic contains a list of issues and an issue can be associated with at most
one epic. When on an epic, you can add its associated issues:
one epic. When you add an issue to an epic that is already associated with another epic,
the issue is automatically removed from the previous epic.
To add an issue to an epic:
1. Click the plus icon (<kbd>+</kbd>) under the epic description.
1. Paste the link of the issue (you can hit <kbd>Spacebar</kbd> to add more than
one issues at a time).
1. Click **Add an issue**.
1. Paste the link of the issue.
- Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
1. Click **Add**.
Any issue belonging to a project in the epic's group or any of the epic's
subgroups are eligible to be added. To remove an issue from an epic, click
on the <kbd>x</kbd> button in the epic's issue list.
To remove an issue from an epic:
NOTE: **Note:**
When you add an issue or an epic to an epic that's already associated with another epic,
the issue or the epic is automatically removed from the previous epic.
1. Click on the <kbd>x</kbd> button in the epic's issue list.
1. Click **Remove** in the **Remove issue** warning message.
## Multi-level child epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7.
Much like adding issues to an epic, an epic can have multiple child epics with
the maximum depth being 5. To add a child epic:
Any epic that belongs to a group, or subgroup of the parent epic's group, is
eligible to be added. New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
When you add a child epic that is already associated with another epic,
that epic is automatically removed from the previous epic.
1. Click the plus icon (<kbd>+</kbd>) under the epic description.
An epic can have multiple child epics with
the maximum depth being 5.
To add a child epic:
1. Click **Add an epic**.
1. Paste the link of the epic.
- Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
1. Click **Add**.
Any epic that belongs to a group or subgroup of the parent epic's group is
eligible to be added. To remove a child epic from a parent epic,
click on the <kbd>x</kbd> button in the parent epic's epic list.
To remove a child epic from a parent epic:
1. Click on the <kbd>x</kbd> button in the parent epic's list of epics.
1. Click **Remove** in the **Remove epic** warning message.
## Start date and due date
For each of the dates in the sidebar of an epic, you can choose to either:
To set a **Start date** and **Due date** for an epic, you can choose either of the following:
- Enter a fixed value.
- Inherit a dynamic value called "From milestones".
- **Fixed**: Enter a fixed value.
- **From milestones:** Inherit a dynamic value from the issues added to the epic.
If you select "From milestones" for the start date, GitLab will automatically set the
If you select **From milestones** for the start date, GitLab will automatically set the
date to be earliest start date across all milestones that are currently assigned
to the issues that are attached to the epic. Similarly, if you select "From milestones"
to the issues that are added to the epic. Similarly, if you select "From milestones"
for the due date, GitLab will set it to be the latest due date across all
milestones that are currently assigned to those issues.
These are dynamic dates in that if milestones are re-assigned to the issues, if the
milestone dates change, or if issues are added or removed from the epic, then
the re-calculation will happen immediately to set a new dynamic date.
These are dynamic dates which are recalculated immediately if any of the following occur:
- Milestones are re-assigned to the issues.
- Milestone dates change.
- Issues are added or removed from the epic.
## Roadmap in epics
## Roadmap
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
If your epic contains one or more [child epics](#multi-level-child-epics) which
have a [start or due date](#start-date-and-due-date), then you can see a
[roadmap](../roadmap/index.md) view of the child epics under the parent epic itself.
have a [start or due date](#start-date-and-due-date), a
[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic.
![Child epics roadmap](img/child_epics_roadmap.png)
![Child epics roadmap](img/epic_view_roadmap_v12.3.png)
## Reordering issues and child epics
Drag and drop to reorder issues and child epics. New issues and child epics added to an epic appear at the top of the list.
New issues and child epics are added to the top of their respective lists in the **Epics and Issues** tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot be intermingled.
To reorder issues assigned to an epic:
1. Go to the **Epics and Issues** tab.
1. Drag and drop issues into the desired order.
To reorder child epics assigned to an epic:
1. Go to the **Epics and Issues** tab.
1. Drag and drop epics into the desired order.
## Updating epics
......
......@@ -26,7 +26,7 @@ Epics in the view can be sorted by:
Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
including the [epics list view](../epics/index.md).
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap).
## Timeline duration
......
......@@ -170,7 +170,7 @@ the `distributionManagement` section:
<repositories>
<repository>
<id>gitlab-maven</id>
<url>https://gitlab.com/api/v4/groups/my-group/-/packages/maven</url>
<url>https://gitlab.com/api/v4/groups/GROUP_ID/-/packages/maven</url>
</repository>
</repositories>
<distributionManagement>
......
......@@ -56,6 +56,16 @@ Click on the service links to see further configuration instructions and details
| [Redmine](redmine.md) | Redmine issue tracker |
| [YouTrack](youtrack.md) | YouTrack issue tracker |
## Push hooks limit
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31009) in GitLab 12.4.
If a single push includes changes to more than three branches or tags, services
supported by `push_hooks` and `tag_push_hooks` events won't be executed.
The number of branches or tags supported can be changed via
[`push_event_hooks_limit` application setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
## Services templates
Services templates is a way to set some predefined values in the Service of
......
......@@ -107,6 +107,9 @@ detailed commit data is expensive. Note that despite only 20 commits being
present in the `commits` attribute, the `total_commits_count` attribute will
contain the actual total.
Also, if a single push includes changes for more than three (by default, depending on
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)) branches, this hook won't be executed.
**Request header**:
```
......@@ -190,6 +193,10 @@ X-Gitlab-Event: Push Hook
Triggered when you create (or delete) tags to the repository.
NOTE: **Note:**
If a single push includes changes for more than three (by default, depending on
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)) tags, this hook won't be executed.
**Request header**:
```
......
......@@ -42,6 +42,88 @@ module API
present deployment, with: Entities::Deployment
end
desc 'Creates a new deployment' do
detail 'This feature was introduced in GitLab 12.4'
success Entities::Deployment
end
params do
requires :environment,
type: String,
desc: 'The name of the environment to deploy to'
requires :sha,
type: String,
desc: 'The SHA of the commit that was deployed'
requires :ref,
type: String,
desc: 'The name of the branch or tag that was deployed'
requires :tag,
type: Boolean,
desc: 'A boolean indicating if the deployment ran for a tag'
requires :status,
type: String,
desc: 'The status of the deployment',
values: %w[running success failed canceled]
end
post ':id/deployments' do
authorize!(:create_deployment, user_project)
authorize!(:create_environment, user_project)
environment = user_project
.environments
.find_or_create_by_name(params[:environment])
unless environment.persisted?
render_validation_error!(deployment)
end
authorize!(:create_deployment, environment)
service = ::Deployments::CreateService
.new(environment, current_user, declared_params)
deployment = service.execute
if deployment.persisted?
present(deployment, with: Entities::Deployment, current_user: current_user)
else
render_validation_error!(deployment)
end
end
desc 'Updates an existing deployment' do
detail 'This feature was introduced in GitLab 12.4'
success Entities::Deployment
end
params do
requires :status,
type: String,
desc: 'The new status of the deployment',
values: %w[running success failed canceled]
end
put ':id/deployments/:deployment_id' do
authorize!(:read_deployment, user_project)
deployment = user_project.deployments.find(params[:deployment_id])
authorize!(:update_deployment, deployment)
if deployment.deployable
forbidden!('Deployments created using GitLab CI can not be updated using the API')
end
service = ::Deployments::UpdateService.new(deployment, declared_params)
if service.execute
present(deployment, with: Entities::Deployment, current_user: current_user)
else
render_validation_error!(deployment)
end
end
end
end
end
......@@ -18,6 +18,7 @@ module API
end
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -26,6 +27,7 @@ module API
members = source.members.where.not(user_id: nil).includes(:user)
members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members = paginate(members)
present members, with: Entities::Member
......@@ -37,6 +39,7 @@ module API
end
params do
optional :query, type: String, desc: 'A query string to search for members'
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
......@@ -45,6 +48,7 @@ module API
members = find_all_members(source_type, source)
members = members.includes(:user).references(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members = paginate(members)
present members, with: Entities::Member
......@@ -68,6 +72,23 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Gets a member of a group or project, including those who gained membership through ancestor group' do
success Entities::Member
end
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/members/all/:user_id" do
source = find_source(source_type, params[:id])
members = find_all_members(source_type, source)
member = members.find_by!(user_id: params[:user_id])
present member, with: Entities::Member
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Adds a member to a group or project.' do
success Entities::Member
end
......
......@@ -101,6 +101,7 @@ module API
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics'
optional :push_event_hooks_limit, type: Integer, desc: "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."
optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
given recaptcha_enabled: ->(val) { val } do
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
......
......@@ -69,14 +69,14 @@ module Gitlab
# means that checking the presence of the License class could result in
# this method returning `false`, even for an EE installation.
#
# The `IS_GITLAB_EE` is always `string` or `nil`
# The `FOSS_ONLY` is always `string` or `nil`
# Thus the nil or empty string will result
# in using default value: true
# in using default value: false
#
# The behavior needs to be synchronised with
# config/helpers/is_ee_env.js
root.join('ee/app/models/license.rb').exist? &&
(ENV['IS_GITLAB_EE'].to_s.empty? || Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE']))
!%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
end
def self.ee
......
# frozen_string_literal: true
# Convert terminal stream to JSON
module Gitlab
module HealthChecks
module Probes
class Liveness
def execute
Probes::Status.new(200, status: 'ok')
end
module Ci
module Ansi2json
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Ansi2json
class Converter
def convert(stream, new_state)
@lines = []
@state = State.new(new_state, stream.size)
append = false
truncated = false
cur_offset = stream.tell
if cur_offset > @state.offset
@state.offset = cur_offset
truncated = true
else
stream.seek(@state.offset)
append = @state.offset > 0
end
start_offset = @state.offset
@state.set_current_line!(style: Style.new(@state.inherited_style))
stream.each_line do |line|
s = StringScanner.new(line)
convert_line(s)
end
# This must be assigned before flushing the current line
# or the @current_line.offset will advance to the very end
# of the trace. Instead we want @last_line_offset to always
# point to the beginning of last line.
@state.set_last_line_offset
flush_current_line
OpenStruct.new(
lines: @lines,
state: @state.encode,
append: append,
truncated: truncated,
offset: start_offset,
size: stream.tell - start_offset,
total: stream.size
)
end
private
def convert_line(scanner)
until scanner.eos?
if scanner.scan(Gitlab::Regex.build_trace_section_regex)
handle_section(scanner)
elsif scanner.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(scanner)
elsif scanner.scan(/\e(([@-_])(.*?)?)?$/)
break
elsif scanner.scan(/</)
@state.current_line << '&lt;'
elsif scanner.scan(/\r?\n/)
# we advance the offset of the next current line
# so it does not start from \n
flush_current_line(advance_offset: scanner.matched_size)
else
@state.current_line << scanner.scan(/./m)
end
@state.offset += scanner.matched_size
end
end
def handle_sequence(scanner)
indicator = scanner[1]
commands = scanner[2].split ';'
terminator = scanner[3]
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
return unless indicator == '[' && terminator == 'm'
@state.update_style(commands)
end
def handle_section(scanner)
action = scanner[1]
timestamp = scanner[2]
section = scanner[3]
section_name = sanitize_section_name(section)
if action == "start"
handle_section_start(section_name, timestamp)
elsif action == "end"
handle_section_end(section_name, timestamp)
end
end
def handle_section_start(section, timestamp)
flush_current_line unless @state.current_line.empty?
@state.open_section(section, timestamp)
end
def handle_section_end(section, timestamp)
return unless @state.section_open?(section)
flush_current_line unless @state.current_line.empty?
@state.close_section(section, timestamp)
# ensure that section end is detached from the last
# line in the section
flush_current_line
end
def flush_current_line(advance_offset: 0)
@lines << @state.current_line.to_h
@state.set_current_line!(advance_offset: advance_offset)
end
def sanitize_section_name(section)
section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Ansi2json
# Line class is responsible for keeping the internal state of
# a log line and to finally serialize it as Hash.
class Line
# Line::Segment is a portion of a line that has its own style
# and text. Multiple segments make the line content.
class Segment
attr_accessor :text, :style
def initialize(style:)
@text = +''
@style = style
end
def empty?
text.empty?
end
def to_h
# Without force encoding to UTF-8 we could get an error
# when serializing the Hash to JSON.
# Encoding::UndefinedConversionError:
# "\xE2" from ASCII-8BIT to UTF-8
{ text: text.force_encoding('UTF-8') }.tap do |result|
result[:style] = style.to_s if style.set?
end
end
end
attr_reader :offset, :sections, :segments, :current_segment,
:section_header, :section_duration
def initialize(offset:, style:, sections: [])
@offset = offset
@segments = []
@sections = sections
@section_header = false
@duration = nil
@current_segment = Segment.new(style: style)
end
def <<(data)
@current_segment.text << data
end
def style
@current_segment.style
end
def empty?
@segments.empty? && @current_segment.empty?
end
def update_style(ansi_commands)
@current_segment.style.update(ansi_commands)
end
def add_section(section)
@sections << section
end
def set_as_section_header
@section_header = true
end
def set_section_duration(duration)
@section_duration = Time.at(duration.to_i).strftime('%M:%S')
end
def flush_current_segment!
return if @current_segment.empty?
@segments << @current_segment.to_h
@current_segment = Segment.new(style: @current_segment.style)
end
def to_h
flush_current_segment!
{ offset: offset, content: @segments }.tap do |result|
result[:section] = sections.last if sections.any?
result[:section_header] = true if @section_header
result[:section_duration] = @section_duration if @section_duration
end
end
end
end
end
end
# frozen_string_literal: true
# This Parser translates ANSI escape codes into human readable format.
# It considers color and format changes.
# Inspired by http://en.wikipedia.org/wiki/ANSI_escape_code
module Gitlab
module Ci
module Ansi2json
class Parser
# keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
COLOR = {
0 => 'black', # not that this is gray in the intense color table
1 => 'red',
2 => 'green',
3 => 'yellow',
4 => 'blue',
5 => 'magenta',
6 => 'cyan',
7 => 'white' # not that this is gray in the dark (aka default) color table
}.freeze
STYLE_SWITCHES = {
bold: 0x01,
italic: 0x02,
underline: 0x04,
conceal: 0x08,
cross: 0x10
}.freeze
def self.bold?(mask)
mask & STYLE_SWITCHES[:bold] != 0
end
def self.matching_formats(mask)
formats = []
STYLE_SWITCHES.each do |text_format, flag|
formats << "term-#{text_format}" if mask & flag != 0
end
formats
end
def initialize(command, ansi_stack = nil)
@command = command
@ansi_stack = ansi_stack
end
def changes
if self.respond_to?("on_#{@command}")
send("on_#{@command}", @ansi_stack) # rubocop:disable GitlabSecurity/PublicSend
end
end
# rubocop:disable Style/SingleLineMethods
def on_0(_) { reset: true } end
def on_1(_) { enable: STYLE_SWITCHES[:bold] } end
def on_3(_) { enable: STYLE_SWITCHES[:italic] } end
def on_4(_) { enable: STYLE_SWITCHES[:underline] } end
def on_8(_) { enable: STYLE_SWITCHES[:conceal] } end
def on_9(_) { enable: STYLE_SWITCHES[:cross] } end
def on_21(_) { disable: STYLE_SWITCHES[:bold] } end
def on_22(_) { disable: STYLE_SWITCHES[:bold] } end
def on_23(_) { disable: STYLE_SWITCHES[:italic] } end
def on_24(_) { disable: STYLE_SWITCHES[:underline] } end
def on_28(_) { disable: STYLE_SWITCHES[:conceal] } end
def on_29(_) { disable: STYLE_SWITCHES[:cross] } end
def on_30(_) { fg: fg_color(0) } end
def on_31(_) { fg: fg_color(1) } end
def on_32(_) { fg: fg_color(2) } end
def on_33(_) { fg: fg_color(3) } end
def on_34(_) { fg: fg_color(4) } end
def on_35(_) { fg: fg_color(5) } end
def on_36(_) { fg: fg_color(6) } end
def on_37(_) { fg: fg_color(7) } end
def on_38(stack) { fg: fg_color_256(stack) } end
def on_39(_) { fg: fg_color(9) } end
def on_40(_) { bg: bg_color(0) } end
def on_41(_) { bg: bg_color(1) } end
def on_42(_) { bg: bg_color(2) } end
def on_43(_) { bg: bg_color(3) } end
def on_44(_) { bg: bg_color(4) } end
def on_45(_) { bg: bg_color(5) } end
def on_46(_) { bg: bg_color(6) } end
def on_47(_) { bg: bg_color(7) } end
def on_48(stack) { bg: bg_color_256(stack) } end
# TODO: all the x9 never get called?
def on_49(_) { fg: fg_color(9) } end
def on_90(_) { fg: fg_color(0, 'l') } end
def on_91(_) { fg: fg_color(1, 'l') } end
def on_92(_) { fg: fg_color(2, 'l') } end
def on_93(_) { fg: fg_color(3, 'l') } end
def on_94(_) { fg: fg_color(4, 'l') } end
def on_95(_) { fg: fg_color(5, 'l') } end
def on_96(_) { fg: fg_color(6, 'l') } end
def on_97(_) { fg: fg_color(7, 'l') } end
def on_99(_) { fg: fg_color(9, 'l') } end
def on_100(_) { fg: bg_color(0, 'l') } end
def on_101(_) { fg: bg_color(1, 'l') } end
def on_102(_) { fg: bg_color(2, 'l') } end
def on_103(_) { fg: bg_color(3, 'l') } end
def on_104(_) { fg: bg_color(4, 'l') } end
def on_105(_) { fg: bg_color(5, 'l') } end
def on_106(_) { fg: bg_color(6, 'l') } end
def on_107(_) { fg: bg_color(7, 'l') } end
def on_109(_) { fg: bg_color(9, 'l') } end
# rubocop:enable Style/SingleLineMethods
def fg_color(color_index, prefix = nil)
term_color_class(color_index, ['fg', prefix])
end
def fg_color_256(command_stack)
xterm_color_class(command_stack, 'fg')
end
def bg_color(color_index, prefix = nil)
term_color_class(color_index, ['bg', prefix])
end
def bg_color_256(command_stack)
xterm_color_class(command_stack, 'bg')
end
def term_color_class(color_index, prefix)
color_name = COLOR[color_index]
return if color_name.nil?
color_class(['term', prefix, color_name])
end
def xterm_color_class(command_stack, prefix)
# the 38 and 48 commands have to be followed by "5" and the color index
return unless command_stack.length >= 2
return unless command_stack[0] == "5"
command_stack.shift # ignore the "5" command
color_index = command_stack.shift.to_i
return unless color_index >= 0
return unless color_index <= 255
color_class(["xterm", prefix, color_index])
end
def color_class(segments)
[segments].flatten.compact.join('-')
end
end
end
end
end
# frozen_string_literal: true
# In this class we keep track of the state changes that the
# Converter makes as it scans through the log stream.
module Gitlab
module Ci
module Ansi2json
class State
attr_accessor :offset, :current_line, :inherited_style, :open_sections, :last_line_offset
def initialize(new_state, stream_size)
@offset = 0
@inherited_style = {}
@open_sections = {}
@stream_size = stream_size
restore_state!(new_state)
end
def encode
state = {
offset: @last_line_offset,
style: @current_line.style.to_h,
open_sections: @open_sections
}
Base64.urlsafe_encode64(state.to_json)
end
def open_section(section, timestamp)
@open_sections[section] = timestamp
@current_line.add_section(section)
@current_line.set_as_section_header
end
def close_section(section, timestamp)
return unless section_open?(section)
duration = timestamp.to_i - @open_sections[section].to_i
@current_line.set_section_duration(duration)
@open_sections.delete(section)
end
def section_open?(section)
@open_sections.key?(section)
end
def set_current_line!(style: nil, advance_offset: 0)
new_line = Line.new(
offset: @offset + advance_offset,
style: style || @current_line.style,
sections: @open_sections.keys
)
@current_line = new_line
end
def set_last_line_offset
@last_line_offset = @current_line.offset
end
def update_style(commands)
@current_line.flush_current_segment!
@current_line.update_style(commands)
end
private
def restore_state!(encoded_state)
state = decode_state(encoded_state)
return unless state
return if state['offset'].to_i > @stream_size
@offset = state['offset'].to_i if state['offset']
@open_sections = state['open_sections'] if state['open_sections']
if state['style']
@inherited_style = {
fg: state.dig('style', 'fg'),
bg: state.dig('style', 'bg'),
mask: state.dig('style', 'mask')
}
end
end
def decode_state(state)
return unless state.present?
decoded_state = Base64.urlsafe_decode64(state)
return unless decoded_state.present?
JSON.parse(decoded_state)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Ansi2json
class Style
attr_reader :fg, :bg, :mask
def initialize(fg: nil, bg: nil, mask: 0)
@fg = fg
@bg = bg
@mask = mask
update_formats
end
def update(ansi_commands)
command = ansi_commands.shift
return unless command
if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
apply_changes(changes)
end
update(ansi_commands)
end
def set?
@fg || @bg || @formats.any?
end
def reset!
@fg = nil
@bg = nil
@mask = 0
@formats = []
end
def ==(other)
self.to_h == other.to_h
end
def to_s
[@fg, @bg, @formats].flatten.compact.join(' ')
end
def to_h
{ fg: @fg, bg: @bg, mask: @mask }
end
private
def apply_changes(changes)
case
when changes[:reset]
reset!
when changes[:fg]
@fg = changes[:fg]
when changes[:bg]
@bg = changes[:bg]
when changes[:enable]
@mask |= changes[:enable]
when changes[:disable]
@mask &= ~changes[:disable]
else
return
end
update_formats
end
def update_formats
# Most terminals show bold colored text in the light color variant
# Let's mimic that here
if @fg.present? && Gitlab::Ci::Ansi2json::Parser.bold?(@mask)
@fg = @fg.sub(/fg-([a-z]{2,}+)/, 'fg-l-\1')
end
@formats = Gitlab::Ci::Ansi2json::Parser.matching_formats(@mask)
end
end
end
end
end
......@@ -12,7 +12,7 @@ module Gitlab
def value
strong_memoize(:value) do
query = @project.deployments.where("created_at >= ?", @from)
query = @project.deployments.success.where("created_at >= ?", @from)
query = query.where("created_at <= ?", @to) if @to
query.count
end
......
......@@ -6,13 +6,13 @@ module Gitlab
include Enumerable
# collection - An array of Gitlab::Diff::Position
def initialize(collection, diff_head_sha)
def initialize(collection, diff_head_sha = nil)
@collection = collection
@diff_head_sha = diff_head_sha
end
def each(&block)
@collection.each(&block)
filtered_positions.each(&block)
end
def concat(positions)
......@@ -23,9 +23,21 @@ module Gitlab
# positions (https://gitlab.com/gitlab-org/gitlab/issues/33271).
def unfoldable
select do |position|
position.unfoldable? && position.head_sha == @diff_head_sha
position.unfoldable? && valid_head_sha?(position)
end
end
private
def filtered_positions
@collection.select { |item| item.is_a?(Position) }
end
def valid_head_sha?(position)
return true unless @diff_head_sha
position.head_sha == @diff_head_sha
end
end
end
end
......@@ -23,15 +23,12 @@ module Gitlab
@parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse
end
def render
contents = @layout.render(self)
write_file(contents)
def contents
# Render and remove an extra trailing new line
@contents ||= @layout.render(self).sub!(/\n(?=\Z)/, '')
end
private
def write_file(contents)
def write
filename = File.join(@output_dir, 'index.md')
FileUtils.mkdir_p(@output_dir)
......
......@@ -20,6 +20,3 @@
- type[:fields].each do |field|
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
\
# frozen_string_literal: true
module Gitlab
module HealthChecks
CHECKS = [
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
end
end
......@@ -3,14 +3,13 @@
module Gitlab
module HealthChecks
module Probes
class Readiness
class Collection
attr_reader :checks
# This accepts an array of objects implementing `:readiness`
# that returns `::Gitlab::HealthChecks::Result`
def initialize(*additional_checks)
@checks = ::Gitlab::HealthChecks::CHECKS
@checks += additional_checks
def initialize(*checks)
@checks = checks
end
def execute
......
......@@ -6,7 +6,7 @@ module Gitlab
class BaseExporter < Daemon
attr_reader :server
attr_accessor :additional_checks
attr_accessor :readiness_checks
def enabled?
settings.enabled
......@@ -73,11 +73,11 @@ module Gitlab
end
def readiness_probe
::Gitlab::HealthChecks::Probes::Readiness.new(*additional_checks)
::Gitlab::HealthChecks::Probes::Collection.new(*readiness_checks)
end
def liveness_probe
::Gitlab::HealthChecks::Probes::Liveness.new
::Gitlab::HealthChecks::Probes::Collection.new
end
def render_probe(probe, req, res)
......
......@@ -20,7 +20,7 @@ module Gitlab
def initialize
super
self.additional_checks = [
self.readiness_checks = [
WebExporter::ExporterCheck.new(self),
Gitlab::HealthChecks::PumaCheck,
Gitlab::HealthChecks::UnicornCheck
......
......@@ -11,10 +11,28 @@ namespace :gitlab do
task compile_docs: :environment do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
renderer.render
renderer.write
puts "Documentation compiled."
end
desc 'GitLab | Check if GraphQL docs are up to date'
task check_docs: :environment do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
doc = File.read(Rails.root.join(OUTPUT_DIR, 'index.md'))
if doc == renderer.contents
puts "GraphQL documentation is up to date"
else
puts '#' * 10
puts '#'
puts '# GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.'
puts '#'
puts '#' * 10
abort
end
end
end
end
......
......@@ -3066,6 +3066,9 @@ msgstr ""
msgid "Choose a type..."
msgstr ""
msgid "Choose an existing tag, or create a new one"
msgstr ""
msgid "Choose any color."
msgstr ""
......@@ -5409,6 +5412,27 @@ msgstr ""
msgid "Deploying to"
msgstr ""
msgid "Deployment|API"
msgstr ""
msgid "Deployment|This deployment was created using the API"
msgstr ""
msgid "Deployment|canceled"
msgstr ""
msgid "Deployment|created"
msgstr ""
msgid "Deployment|failed"
msgstr ""
msgid "Deployment|running"
msgstr ""
msgid "Deployment|success"
msgstr ""
msgid "Deprioritize label"
msgstr ""
......@@ -5766,6 +5790,9 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
msgid "Edit Release"
msgstr ""
msgid "Edit Snippet"
msgstr ""
......@@ -11172,6 +11199,9 @@ msgstr ""
msgid "Number of LOCs per commit"
msgstr ""
msgid "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."
msgstr ""
msgid "Number of commits per MR"
msgstr ""
......@@ -13456,12 +13486,27 @@ msgstr ""
msgid "Release"
msgstr ""
msgid "Release notes"
msgstr ""
msgid "Release title"
msgstr ""
msgid "Releases"
msgstr ""
msgid "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}."
msgstr ""
msgid "Releases mark specific points in a project's development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API."
msgstr ""
msgid "Release|Something went wrong while getting the release details"
msgstr ""
msgid "Release|Something went wrong while saving the release details"
msgstr ""
msgid "Remember me"
msgstr ""
......@@ -15943,6 +15988,9 @@ msgstr ""
msgid "Tag list:"
msgstr ""
msgid "Tag name"
msgstr ""
msgid "Tag this commit."
msgstr ""
......@@ -18683,6 +18731,9 @@ msgstr ""
msgid "Write milestone description..."
msgstr ""
msgid "Write your release notes or drag your files here…"
msgstr ""
msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
msgstr ""
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment