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

Add latest changes from gitlab-org/gitlab@master

parent 00c78fb8
VERSION merge=ours VERSION merge=ours
Dangerfile gitlab-language=ruby Dangerfile gitlab-language=ruby
db/schema.rb merge=merge_db_schema
...@@ -67,3 +67,18 @@ docs lint: ...@@ -67,3 +67,18 @@ docs lint:
- bundle exec nanoc check internal_links - bundle exec nanoc check internal_links
# Check the internal anchor links # Check the internal anchor links
- bundle exec nanoc check internal_anchors - 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 @@ ...@@ -53,7 +53,7 @@
- gitlab-org - gitlab-org
- docker - docker
gitlab:assets:compile: gitlab:assets:compile pull-push-cache:
extends: .gitlab:assets:compile-metadata extends: .gitlab:assets:compile-metadata
only: only:
refs: refs:
...@@ -63,9 +63,6 @@ gitlab:assets:compile: ...@@ -63,9 +63,6 @@ gitlab:assets:compile:
gitlab:assets:compile pull-cache: gitlab:assets:compile pull-cache:
extends: .gitlab:assets:compile-metadata extends: .gitlab:assets:compile-metadata
except:
refs:
- master
cache: cache:
policy: pull policy: pull
...@@ -89,14 +86,14 @@ gitlab:assets:compile pull-cache: ...@@ -89,14 +86,14 @@ gitlab:assets:compile pull-cache:
# we override the max_old_space_size to prevent OOM errors # we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584 NODE_OPTIONS: --max_old_space_size=3584
cache: cache:
key: "assets-compile:test:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v6" key: "assets-compile:v7"
artifacts: artifacts:
expire_in: 7d expire_in: 7d
paths: paths:
- node_modules - node_modules
- public/assets - public/assets
compile-assets: compile-assets pull-push-cache:
extends: .compile-assets-metadata extends: .compile-assets-metadata
only: only:
refs: refs:
...@@ -104,13 +101,25 @@ compile-assets: ...@@ -104,13 +101,25 @@ compile-assets:
cache: cache:
policy: pull-push policy: pull-push
compile-assets pull-cache: compile-assets pull-push-cache foss:
extends: .compile-assets-metadata extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
except: only:
refs: refs:
- master - 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: cache:
policy: pull policy: pull
key: "assets-compile:v7:foss"
.only-code-frontend-job-base: .only-code-frontend-job-base:
extends: extends:
...@@ -121,7 +130,9 @@ compile-assets pull-cache: ...@@ -121,7 +130,9 @@ compile-assets pull-cache:
- .default-before_script - .default-before_script
- .only-code-changes - .only-code-changes
- .use-pg9 - .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: .karma-base:
extends: .only-code-frontend-job-base extends: .only-code-frontend-job-base
...@@ -195,6 +206,7 @@ jest-foss: ...@@ -195,6 +206,7 @@ jest-foss:
- .default-cache - .default-cache
- .default-only - .default-only
- .only-code-changes - .only-code-changes
stage: test
dependencies: [] dependencies: []
cache: cache:
key: "$CI_JOB_NAME" key: "$CI_JOB_NAME"
...@@ -227,7 +239,9 @@ webpack-dev-server: ...@@ -227,7 +239,9 @@ webpack-dev-server:
- .default-cache - .default-cache
- .default-only - .default-only
- .only-code-changes - .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: variables:
WEBPACK_MEMORY_TEST: "true" WEBPACK_MEMORY_TEST: "true"
script: script:
......
...@@ -71,6 +71,12 @@ ...@@ -71,6 +71,12 @@
- "doc/**/*" - "doc/**/*"
- ".markdownlint.json" - ".markdownlint.json"
.only-graphql-changes:
only:
changes:
- "{,ee/}app/graphql/**/*"
- "{,ee/}lib/gitlab/graphql/**/*"
.only-code-qa-changes: .only-code-qa-changes:
only: only:
changes: changes:
...@@ -153,4 +159,4 @@ ...@@ -153,4 +159,4 @@
.only-ee-as-if-foss: .only-ee-as-if-foss:
extends: .only-ee extends: .only-ee
variables: variables:
IS_GITLAB_EE: '0' FOSS_ONLY: '1'
...@@ -11,7 +11,7 @@ pages: ...@@ -11,7 +11,7 @@ pages:
variables: variables:
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
stage: pages stage: pages
dependencies: ["coverage", "karma", "gitlab:assets:compile"] dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
script: script:
- mv public/ .public/ - mv public/ .public/
- mkdir public/ - mkdir public/
......
...@@ -71,4 +71,4 @@ schedule:package-and-qa: ...@@ -71,4 +71,4 @@ schedule:package-and-qa:
- .package-and-qa-base - .package-and-qa-base
- .only-code-qa-changes - .only-code-qa-changes
- .only-canonical-schedules - .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: ...@@ -53,6 +53,8 @@ setup-test-env:
.rspec-base: .rspec-base:
extends: .only-code-rails-job-base extends: .only-code-rails-job-base
stage: test 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: script:
- source scripts/rspec_helpers.sh - source scripts/rspec_helpers.sh
- rspec_paralellized_job "--tag ~quarantine --tag ~geo" - rspec_paralellized_job "--tag ~quarantine --tag ~geo"
...@@ -69,6 +71,11 @@ setup-test-env: ...@@ -69,6 +71,11 @@ setup-test-env:
reports: reports:
junit: junit_rspec.xml 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: .rspec-base-pg9:
extends: extends:
- .rspec-base - .rspec-base
...@@ -76,9 +83,8 @@ setup-test-env: ...@@ -76,9 +83,8 @@ setup-test-env:
.rspec-base-pg9-foss: .rspec-base-pg9-foss:
extends: extends:
- .rspec-base - .rspec-base-foss
- .use-pg9 - .use-pg9
- .only-ee-as-if-foss
.rspec-base-pg10: .rspec-base-pg10:
extends: extends:
...@@ -106,10 +112,9 @@ rspec system pg9: ...@@ -106,10 +112,9 @@ rspec system pg9:
extends: .rspec-base-pg9 extends: .rspec-base-pg9
parallel: 24 parallel: 24
# TODO: This requires FOSS assets rspec system pg9-foss:
# rspec system pg9-foss: extends: .rspec-base-pg9-foss
# extends: .rspec-base-pg9-foss parallel: 24
# parallel: 24
rspec unit pg10: rspec unit pg10:
extends: .rspec-base-pg10 extends: .rspec-base-pg10
...@@ -229,7 +234,9 @@ rspec fast_spec_helper: ...@@ -229,7 +234,9 @@ rspec fast_spec_helper:
static-analysis: static-analysis:
extends: .only-code-qa-rails-job-base 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: variables:
SETUP_DB: "false" SETUP_DB: "false"
script: script:
...@@ -252,16 +259,16 @@ downtime_check: ...@@ -252,16 +259,16 @@ downtime_check:
variables: variables:
- $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ - $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
stage: test stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"] needs: ["setup-test-env"]
dependencies: ["setup-test-env"]
.db-job-base: .db-job-base:
extends: extends:
- .only-code-rails-job-base - .only-code-rails-job-base
- .use-pg9 - .use-pg9
stage: test stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"] needs: ["setup-test-env"]
dependencies: ["setup-test-env"]
# DB migration, rollback, and seed jobs # DB migration, rollback, and seed jobs
db:migrate:reset: db:migrate:reset:
......
...@@ -81,7 +81,7 @@ schedule:review-build-cng: ...@@ -81,7 +81,7 @@ schedule:review-build-cng:
extends: extends:
- .review-build-cng-base - .review-build-cng-base
- .only-review-schedules - .only-review-schedules
needs: ["gitlab:assets:compile"] needs: ["gitlab:assets:compile pull-cache"]
.review-deploy-base: .review-deploy-base:
extends: extends:
...@@ -97,7 +97,7 @@ schedule:review-build-cng: ...@@ -97,7 +97,7 @@ schedule:review-build-cng:
variables: variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master" GITLAB_HELM_CHART_REF: "v2.3.7"
GITLAB_EDITION: "ce" GITLAB_EDITION: "ce"
environment: environment:
name: review/${CI_COMMIT_REF_NAME} name: review/${CI_COMMIT_REF_NAME}
......
...@@ -36,6 +36,7 @@ const Api = { ...@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases', 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', mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: 'api/:version/application/statistics', adminStatisticsPath: 'api/:version/application/statistics',
...@@ -391,6 +392,22 @@ const Api = { ...@@ -391,6 +392,22 @@ const Api = {
return axios.get(url); 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() { adminStatistics() {
const url = Api.buildUrl(this.adminStatisticsPath); const url = Api.buildUrl(this.adminStatisticsPath);
return axios.get(url); 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 { ...@@ -123,7 +123,7 @@ ul.content-list {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
a:not(.default-link-color) { a {
color: $gl-text-color; color: $gl-text-color;
} }
......
.tag-release-link {
color: $blue-600 !important;
}
...@@ -4,18 +4,31 @@ class HealthController < ActionController::Base ...@@ -4,18 +4,31 @@ class HealthController < ActionController::Base
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
include RequiresWhitelistedMonitoringClient 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 def readiness
render_probe(::Gitlab::HealthChecks::Probes::Readiness) # readiness check is a collection with all above application-level checks
render_checks(*CHECKS)
end end
def liveness def liveness
render_probe(::Gitlab::HealthChecks::Probes::Liveness) # liveness check is a collection without additional checks
render_checks
end end
private private
def render_probe(probe_class) def render_checks(*checks)
result = probe_class.new.execute 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 # 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? headers["X-GitLab-Custom-Error"] = 1 unless result.success?
......
...@@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController ...@@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController
@deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment) @deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
end end
# rubocop: disable CodeReuse/ActiveRecord
def deployment def deployment
@deployment ||= environment.deployments.find_by(iid: params[:id]) @deployment ||= environment.deployments.find_successful_deployment!(params[:id])
end end
# rubocop: enable CodeReuse/ActiveRecord
def environment def environment
@environment ||= project.environments.find(params[:environment_id]) @environment ||= project.environments.find(params[:environment_id])
......
...@@ -51,9 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -51,9 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs def render_diffs
@environment = @merge_request.environments_for(current_user).last @environment = @merge_request.environments_for(current_user).last
note_positions = renderable_notes.map(&:position).compact @diffs.unfold_diff_files(note_positions.unfoldable)
@diffs.unfold_diff_files(note_positions)
@diffs.write_cache @diffs.write_cache
request = { request = {
...@@ -140,6 +138,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -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) @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
end end
def note_positions
@note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
end
def renderable_notes def renderable_notes
define_diff_comment_vars unless @notes define_diff_comment_vars unless @notes
......
...@@ -289,7 +289,8 @@ module ApplicationSettingsHelper ...@@ -289,7 +289,8 @@ module ApplicationSettingsHelper
:snowplow_collector_hostname, :snowplow_collector_hostname,
:snowplow_cookie_domain, :snowplow_cookie_domain,
:snowplow_enabled, :snowplow_enabled,
:snowplow_site_id :snowplow_site_id,
:push_event_hooks_limit
] ]
end end
......
...@@ -18,12 +18,16 @@ module EnvironmentHelper ...@@ -18,12 +18,16 @@ module EnvironmentHelper
end end
end end
def deployment_path(deployment)
[deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
end
def deployment_link(deployment, text: nil) def deployment_link(deployment, text: nil)
return unless deployment return unless deployment
link_label = text ? text : "##{deployment.iid}" 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 end
def last_deployment_link_for_environment_build(project, build) def last_deployment_link_for_environment_build(project, build)
...@@ -32,4 +36,31 @@ module EnvironmentHelper ...@@ -32,4 +36,31 @@ module EnvironmentHelper
deployment_link(environment.last_deployment) deployment_link(environment.last_deployment)
end 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 end
...@@ -19,4 +19,14 @@ module ReleasesHelper ...@@ -19,4 +19,14 @@ module ReleasesHelper
documentation_path: help_page documentation_path: help_page
} }
end 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 end
...@@ -214,6 +214,9 @@ class ApplicationSetting < ApplicationRecord ...@@ -214,6 +214,9 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') }, length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false allow_nil: false
validates :push_event_hooks_limit,
numericality: { greater_than_or_equal_to: 0 }
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
......
...@@ -82,6 +82,7 @@ module ApplicationSettingImplementation ...@@ -82,6 +82,7 @@ module ApplicationSettingImplementation
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
project_export_enabled: true, project_export_enabled: true,
protected_ci_variables: false, protected_ci_variables: false,
push_event_hooks_limit: 3,
raw_blob_request_limit: 300, raw_blob_request_limit: 300,
recaptcha_enabled: false, recaptcha_enabled: false,
login_recaptcha_protection_enabled: false, login_recaptcha_protection_enabled: false,
......
...@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord ...@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
belongs_to :environment, required: true belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user 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 has_internal_id :iid, scope: :project, init: ->(s) do
Deployment.where(project: s.project).maximum(:iid) if s&.project Deployment.where(project: s.project).maximum(:iid) if s&.project
...@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord ...@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment, -> (environment) { where(environment_id: environment) }
scope :visible, -> { where(status: %i[running success failed canceled]) }
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :run do event :run do
transition created: :running transition created: :running
...@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord ...@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord
find(ids) find(ids)
end end
def self.find_successful_deployment!(iid)
success.find_by!(iid: iid)
end
def commit def commit
project.commit(sha) project.commit(sha)
end end
......
...@@ -6,7 +6,8 @@ class Environment < ApplicationRecord ...@@ -6,7 +6,8 @@ class Environment < ApplicationRecord
belongs_to :project, required: true 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' has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
...@@ -81,6 +82,10 @@ class Environment < ApplicationRecord ...@@ -81,6 +82,10 @@ class Environment < ApplicationRecord
pluck(:name) pluck(:name)
end end
def self.find_or_create_by_name(name)
find_or_create_by(name: name)
end
def predefined_variables def predefined_variables
Gitlab::Ci::Variables::Collection.new Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name) .append(key: 'CI_ENVIRONMENT_NAME', value: name)
......
...@@ -281,7 +281,7 @@ class Project < ApplicationRecord ...@@ -281,7 +281,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable' has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger' has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments has_many :environments
has_many :deployments, -> { success } has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens
......
...@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy ...@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
can?(:update_build, @subject.deployable) can?(:update_build, @subject.deployable)
end 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 :create_deployment
prevent :update_deployment prevent :update_deployment
end end
rule { ~can_update_deployment }.policy do
prevent :update_deployment
end
end end
...@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy ...@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image enable :destroy_container_image
enable :create_environment enable :create_environment
enable :create_deployment enable :create_deployment
enable :update_deployment
enable :create_release enable :create_release
enable :update_release enable :update_release
end 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 ...@@ -62,6 +62,8 @@ module Git
end end
def execute_project_hooks def execute_project_hooks
return unless params.fetch(:execute_project_hooks, true)
# Creating push_data invokes one CommitDelta RPC per commit. Only # Creating push_data invokes one CommitDelta RPC per commit. Only
# build this data if we actually need it. # build this data if we actually need it.
project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name) project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
......
...@@ -17,7 +17,7 @@ module Git ...@@ -17,7 +17,7 @@ module Git
changes_by_action = group_changes_by_action(changes) changes_by_action = group_changes_by_action(changes)
changes_by_action.each do |_, 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
end end
...@@ -34,7 +34,11 @@ module Git ...@@ -34,7 +34,11 @@ module Git
:pushed :pushed
end 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) push_service_class = push_service_class_for(ref_type)
changes.each do |change| changes.each do |change|
...@@ -43,7 +47,8 @@ module Git ...@@ -43,7 +47,8 @@ module Git
current_user, current_user,
change: change, change: change,
push_options: params[:push_options], 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 ).execute
end end
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 @@ ...@@ -20,5 +20,10 @@
= f.number_field :raw_blob_request_limit, class: 'form-control' = f.number_field :raw_blob_request_limit, class: 'form-control'
.form-text.text-muted .form-text.text-muted
= _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.') = _('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" = f.submit 'Save changes', class: "btn btn-success"
- breadcrumb_title "Repository" - breadcrumb_title "Repository"
- page_title @blob.path, @ref - 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 } } .js-signature-container{ data: { 'signatures-path': signatures_path } }
......
.gl-responsive-table-row.deployment{ role: 'row' } .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-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("ID") .table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid} %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") .table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment = 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") .table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable - if deployment.deployable
.table-mobile-content .table-mobile-content
.flex-truncate-parent .flex-truncate-parent
.flex-truncate-child .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}) #{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.deployed_by - else
%div .badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
by = s_('Deployment|API')
= user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
.table-section.section-15{ role: 'gridcell' } .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created") .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 - 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 .btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', 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') - 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 = 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? - if deployment.last?
......
...@@ -60,10 +60,13 @@ ...@@ -60,10 +60,13 @@
.table-holder .table-holder
.ci-table.environments{ role: 'grid' } .ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' } .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-10{ role: 'columnheader' }= _('ID')
.table-section.section-30{ role: 'columnheader' }= _('Commit') .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
.table-section.section-25{ role: 'columnheader' }= _('Job') .table-section.section-25{ role: 'columnheader' }= _('Commit')
.table-section.section-15{ role: 'columnheader' }= _('Created') .table-section.section-10{ role: 'columnheader' }= _('Job')
.table-section.section-10{ role: 'columnheader' }= _('Created')
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
= render @deployments = render @deployments
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon), %button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
data: { toggle: 'modal', target: '.issues-import-modal' } } data: { toggle: 'modal', target: '.issues-import-modal' } }
- if type == :icon - if type == :icon
= sprite_icon('upload') = sprite_icon('import')
- else - else
= _('Import CSV') = _('Import CSV')
- page_title _('Edit Release')
#js-edit-release-page{ data: data_for_edit_release_page }
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
.text-secondary .text-secondary
= icon('rocket') = icon('rocket')
= _("Release") = _("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? - if release.description.present?
.description.md.prepend-top-default .description.md.prepend-top-default
= markdown_field(release, :description) = 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 = link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
= icon('rss') = sprite_icon('rss')
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do = 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 ...@@ -10,7 +10,7 @@ module Deployments
Deployment.find_by_id(deployment_id).try do |deployment| Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success? break unless deployment.success?
UpdateDeploymentService.new(deployment).execute Deployments::AfterCreateService.new(deployment).execute
end end
end 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'); ...@@ -3,12 +3,12 @@ const path = require('path');
const ROOT_PATH = path.resolve(__dirname, '../..'); 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 // 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 // The behavior needs to be synchronised with
// lib/gitlab.rb: Gitlab.ee? // lib/gitlab.rb: Gitlab.ee?
const isFossOnly = JSON.parse(process.env.FOSS_ONLY || 'false');
module.exports = module.exports =
fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) && fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) && !isFossOnly;
(!process.env.IS_GITLAB_EE || JSON.parse(process.env.IS_GITLAB_EE));
...@@ -380,7 +380,7 @@ module.exports = { ...@@ -380,7 +380,7 @@ module.exports = {
new webpack.DefinePlugin({ new webpack.DefinePlugin({
// This one is used to define window.gon.ee and other things properly in tests: // 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 // This one is used to check against "EE" properly in application code
IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false), 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 ...@@ -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.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_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", 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 ["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 ["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" 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 - ...@@ -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 how active your users are, how much automation you use, mirroring, and
repo/change size. repo/change size.
- 3 PostgreSQL - 4 CPU, 16GiB memory per node | Service | Configuration | GCP type |
- 1 PgBouncer - 2 CPU, 4GiB memory | ------------------------------|-------------------------|----------------|
- 2 Redis - 2 CPU, 8GiB memory per node | 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 Consul/Sentinel - 2 CPU, 2GiB memory per node | 3 PostgreSQL | 4 vCPU, 15GB Memory | n1-standard-4 |
- 4 Sidekiq - 4 CPU, 16GiB memory per node | 1 PgBouncer | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
- 5 GitLab application nodes - 16 CPU, 64GiB memory per node | 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 |
- 1 Gitaly - 16 CPU, 64GiB memory | 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
- 1 Monitoring node - 2 CPU, 8GiB memory, 100GiB local storage | 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 ### 25,000 User Configuration
...@@ -249,7 +254,7 @@ adjusted prior to certification based on performance testing. ...@@ -249,7 +254,7 @@ adjusted prior to certification based on performance testing.
| 3 Redis Persistent + Sentinel | 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 | | 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | | 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 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | | 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
...@@ -277,15 +282,15 @@ testing. ...@@ -277,15 +282,15 @@ testing.
| 3 Redis Persistent + Sentinel | 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 | | 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | | 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 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | | 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 [^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 nodes as an absolute minimum for performance at the 10,000 and 25,000 user
4 nodes as an absolute minimum at the 50,000 user scale, but additional scale and 4 nodes as an absolute minimum at the 50,000 user scale, but
nodes should be considered in conjunction with a review of project counts additional nodes should be considered in conjunction with a review of
and sizes. project counts and sizes.
[^2]: HAProxy is the only tested and recommended load balancer. Additional [^2]: HAProxy is the only tested and recommended load balancer. Additional
options may be supported in the future. options may be supported in the future.
...@@ -223,3 +223,100 @@ Example of response ...@@ -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 ...@@ -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 | | `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 | | `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 ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members
...@@ -62,9 +63,8 @@ Example response: ...@@ -62,9 +63,8 @@ Example response:
## List all members of a group or project including inherited members ## 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. 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) 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. 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.
``` ```
GET /groups/:id/members/all GET /groups/:id/members/all
...@@ -75,6 +75,7 @@ GET /projects/: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 | | `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 | | `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 ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all
...@@ -120,7 +121,7 @@ Example response: ...@@ -120,7 +121,7 @@ Example response:
## Get a member of a group or project ## 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 GET /groups/:id/members/:user_id
...@@ -152,6 +153,42 @@ Example response: ...@@ -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 ## Add a member to a group or project
Adds 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. ...@@ -289,6 +289,7 @@ are listed in the descriptions of the relevant settings.
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. | | `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. | | `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. | `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_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_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. |
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. | | `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |
......
...@@ -14,7 +14,7 @@ tasks such as: ...@@ -14,7 +14,7 @@ tasks such as:
To request access to Chatops on GitLab.com: 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. 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 ## See also
......
...@@ -492,19 +492,50 @@ For other punctuation rules, please refer to the ...@@ -492,19 +492,50 @@ For other punctuation rules, please refer to the
- Use inline link markdown markup `[Text](https://example.com)`. - Use inline link markdown markup `[Text](https://example.com)`.
It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`. 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/). - 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)`, E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](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 ### Links requiring permissions
Don't link directly to: Don't link directly to:
......
...@@ -20,9 +20,9 @@ should be added for EE. Licensed features can be stubbed using the ...@@ -20,9 +20,9 @@ should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`. spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
You can force GitLab to act as CE by either deleting the `ee/` directory or by 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) 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 `false`. The same works for running tests to something that evaluates as `true`. The same works for running tests
(for example `IS_GITLAB_EE=0 yarn jest`). (for example `FOSS_ONLY=1 yarn jest`).
[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab/issues/2500 [ee-as-ce]: https://gitlab.com/gitlab-org/gitlab/issues/2500
......
...@@ -102,7 +102,7 @@ These common definitions are: ...@@ -102,7 +102,7 @@ These common definitions are:
`docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services. `docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services.
- `.only-ee`: Only creates a job for the `gitlab` project. - `.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 - `.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 ## Changes detection
...@@ -115,6 +115,7 @@ from a commit or MR by extending from the following CI definitions: ...@@ -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-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-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-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> **See <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml>
for the list of exact patterns.** for the list of exact patterns.**
...@@ -127,7 +128,7 @@ execute jobs out of order for the following jobs: ...@@ -127,7 +128,7 @@ execute jobs out of order for the following jobs:
```mermaid ```mermaid
graph RL; graph RL;
A[setup-test-env]; 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]; C[gitlab:assets:compile pull-cache];
D["cache gems<br/>(master and tags only)"]; D["cache gems<br/>(master and tags only)"];
E[review-build-cng]; E[review-build-cng];
...@@ -136,7 +137,7 @@ graph RL; ...@@ -136,7 +137,7 @@ graph RL;
G2["schedule:review-deploy<br/>(master only)"]; G2["schedule:review-deploy<br/>(master only)"];
H[karma]; H[karma];
I[jest]; I[jest];
J["compile-assets<br/>(master only)"]; J["compile-assets pull-push-cache<br/>(master only)"];
K[compile-assets pull-cache]; K[compile-assets pull-cache];
L[webpack-dev-server]; L[webpack-dev-server];
M[coverage]; M[coverage];
...@@ -145,39 +146,42 @@ graph RL; ...@@ -145,39 +146,42 @@ graph RL;
P["schedule:package-and-qa<br/>(master schedule only)"]; P["schedule:package-and-qa<br/>(master schedule only)"];
Q[package-and-qa]; Q[package-and-qa];
R[package-and-qa-manual]; R[package-and-qa-manual];
S["RSpec<br/>(e.g. rspec unit pg9)"]
T[retrieve-tests-metadata];
subgraph "`prepare` stage" subgraph "`prepare` stage"
A A
F F
J
K K
J
T
end end
subgraph "`test` stage" subgraph "`test` stage"
B --> |needs| A; B --> |needs| A;
C --> |needs| A; C --> |needs| A;
D --> |needs| A; D --> |needs| A;
H -.-> |depends on| A; H -.-> |needs and depends on| A;
H -.-> |depends on| J; H -.-> |needs and depends on| K;
H -.-> |depends on| K; I -.-> |needs and depends on| A;
I -.-> |depends on| A; I -.-> |needs and depends on| K;
I -.-> |depends on| J; L -.-> |needs and depends on| A;
I -.-> |depends on| K; L -.-> |needs and depends on| K;
L -.-> |depends on| A; O -.-> |needs and depends on| A;
L -.-> |depends on| J; O -.-> |needs and depends on| K;
L -.-> |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; downtime_check --> |needs and depends on| A;
db:* --> |needs| A; db:* --> |needs| A;
gitlab:setup --> |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; downtime_check --> |needs and depends on| A;
graphql-docs-verify --> |needs| A;
end end
subgraph "`review-prepare` stage" subgraph "`review-prepare` stage"
E --> |needs| C; 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 end
subgraph "`review` stage" subgraph "`review` stage"
...@@ -190,7 +194,7 @@ subgraph "`qa` stage" ...@@ -190,7 +194,7 @@ subgraph "`qa` stage"
Q --> |needs| F; Q --> |needs| F;
R --> |needs| C; R --> |needs| C;
R --> |needs| F; R --> |needs| F;
P --> |needs| B; P --> |needs| C;
P --> |needs| F; P --> |needs| F;
review-qa-smoke -.-> |needs and depends on| G; review-qa-smoke -.-> |needs and depends on| G;
review-qa-all -.-> |needs and depends on| G; review-qa-all -.-> |needs and depends on| G;
...@@ -209,7 +213,7 @@ subgraph "`post-test` stage" ...@@ -209,7 +213,7 @@ subgraph "`post-test` stage"
end end
subgraph "`pages` stage" subgraph "`pages` stage"
N -.-> |depends on| B; N -.-> |depends on| C;
N -.-> |depends on| H; N -.-> |depends on| H;
N -.-> |depends on| M; N -.-> |depends on| M;
end end
......
...@@ -10,13 +10,13 @@ Epics let you manage your portfolio of projects more efficiently and with less ...@@ -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 effort by tracking groups of issues that share a theme, across projects and
milestones. milestones.
![epics list view](img/epics_list_view.png) ![epics list view](img/epics_list_view_v12.3.png)
## Use cases ## 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. - 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. - 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 ## Creating an epic
...@@ -24,78 +24,114 @@ A paginated list of epics is available in each group from where you can create ...@@ -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 a new epic. The list of epics includes also epics from all subgroups of the
selected group. From your group page: selected group. From your group page:
1. Go to **Epics** 1. Go to **Epics**.
1. Click the **New epic** button at the top right 1. Click **New epic**.
1. Enter a descriptive title and hit **Create 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 will be taken to the new epic where can edit the following details:
you can change its title, description, start date, and due date.
![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 ## 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 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. Click **Add an issue**.
1. Paste the link of the issue (you can hit <kbd>Spacebar</kbd> to add more than 1. Paste the link of the issue.
one issues at a time). - Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
1. Click **Add**. 1. Click **Add**.
Any issue belonging to a project in the epic's group or any of the epic's To remove an issue from an epic:
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.
NOTE: **Note:** 1. Click on the <kbd>x</kbd> button in the epic's issue list.
When you add an issue or an epic to an epic that's already associated with another epic, 1. Click **Remove** in the **Remove issue** warning message.
the issue or the epic is automatically removed from the previous epic.
## Multi-level child epics ## Multi-level child epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7. > [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 Any epic that belongs to a group, or subgroup of the parent epic's group, is
the maximum depth being 5. To add a child epic: 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. 1. Paste the link of the epic.
- Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
1. Click **Add**. 1. Click **Add**.
Any epic that belongs to a group or subgroup of the parent epic's group is To remove a child epic from a parent epic:
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. 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 ## 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. - **Fixed**: Enter a fixed value.
- Inherit a dynamic value called "From milestones". - **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 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 for the due date, GitLab will set it to be the latest due date across all
milestones that are currently assigned to those issues. milestones that are currently assigned to those issues.
These are dynamic dates in that if milestones are re-assigned to the issues, if the These are dynamic dates which are recalculated immediately if any of the following occur:
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. - 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. > [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 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 have a [start or due date](#start-date-and-due-date), a
[roadmap](../roadmap/index.md) view of the child epics under the parent epic itself. [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 ## 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 ## Updating epics
......
...@@ -26,7 +26,7 @@ Epics in the view can be sorted by: ...@@ -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, 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). 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 ## Timeline duration
......
...@@ -170,7 +170,7 @@ the `distributionManagement` section: ...@@ -170,7 +170,7 @@ the `distributionManagement` section:
<repositories> <repositories>
<repository> <repository>
<id>gitlab-maven</id> <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> </repository>
</repositories> </repositories>
<distributionManagement> <distributionManagement>
......
...@@ -56,6 +56,16 @@ Click on the service links to see further configuration instructions and details ...@@ -56,6 +56,16 @@ Click on the service links to see further configuration instructions and details
| [Redmine](redmine.md) | Redmine issue tracker | | [Redmine](redmine.md) | Redmine issue tracker |
| [YouTrack](youtrack.md) | YouTrack 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
Services templates is a way to set some predefined values in the Service of 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 ...@@ -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 present in the `commits` attribute, the `total_commits_count` attribute will
contain the actual total. 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**: **Request header**:
``` ```
...@@ -190,6 +193,10 @@ X-Gitlab-Event: Push Hook ...@@ -190,6 +193,10 @@ X-Gitlab-Event: Push Hook
Triggered when you create (or delete) tags to the repository. 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**: **Request header**:
``` ```
......
...@@ -42,6 +42,88 @@ module API ...@@ -42,6 +42,88 @@ module API
present deployment, with: Entities::Deployment present deployment, with: Entities::Deployment
end 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 end
end end
...@@ -18,6 +18,7 @@ module API ...@@ -18,6 +18,7 @@ module API
end end
params do params do
optional :query, type: String, desc: 'A query string to search for members' 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 use :pagination
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -26,6 +27,7 @@ module API ...@@ -26,6 +27,7 @@ module API
members = source.members.where.not(user_id: nil).includes(:user) 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.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) members = paginate(members)
present members, with: Entities::Member present members, with: Entities::Member
...@@ -37,6 +39,7 @@ module API ...@@ -37,6 +39,7 @@ module API
end end
params do params do
optional :query, type: String, desc: 'A query string to search for members' 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 use :pagination
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -45,6 +48,7 @@ module API ...@@ -45,6 +48,7 @@ module API
members = find_all_members(source_type, source) members = find_all_members(source_type, source)
members = members.includes(:user).references(:user).merge(User.search(params[:query])) if params[:query].present? 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) members = paginate(members)
present members, with: Entities::Member present members, with: Entities::Member
...@@ -68,6 +72,23 @@ module API ...@@ -68,6 +72,23 @@ module API
end end
# rubocop: enable CodeReuse/ActiveRecord # 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 desc 'Adds a member to a group or project.' do
success Entities::Member success Entities::Member
end end
......
...@@ -101,6 +101,7 @@ module API ...@@ -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 :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 :project_export_enabled, type: Boolean, desc: 'Enable project export'
optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics' 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' optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
given recaptcha_enabled: ->(val) { val } do given recaptcha_enabled: ->(val) { val } do
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
......
...@@ -69,14 +69,14 @@ module Gitlab ...@@ -69,14 +69,14 @@ module Gitlab
# means that checking the presence of the License class could result in # means that checking the presence of the License class could result in
# this method returning `false`, even for an EE installation. # 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 # 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 # The behavior needs to be synchronised with
# config/helpers/is_ee_env.js # config/helpers/is_ee_env.js
root.join('ee/app/models/license.rb').exist? && 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 end
def self.ee def self.ee
......
# frozen_string_literal: true # frozen_string_literal: true
# Convert terminal stream to JSON
module Gitlab module Gitlab
module HealthChecks module Ci
module Probes module Ansi2json
class Liveness def self.convert(ansi, state = nil)
def execute Converter.new.convert(ansi, state)
Probes::Status.new(200, status: 'ok')
end
end end
end 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 ...@@ -12,7 +12,7 @@ module Gitlab
def value def value
strong_memoize(:value) do 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 = query.where("created_at <= ?", @to) if @to
query.count query.count
end end
......
...@@ -6,13 +6,13 @@ module Gitlab ...@@ -6,13 +6,13 @@ module Gitlab
include Enumerable include Enumerable
# collection - An array of Gitlab::Diff::Position # collection - An array of Gitlab::Diff::Position
def initialize(collection, diff_head_sha) def initialize(collection, diff_head_sha = nil)
@collection = collection @collection = collection
@diff_head_sha = diff_head_sha @diff_head_sha = diff_head_sha
end end
def each(&block) def each(&block)
@collection.each(&block) filtered_positions.each(&block)
end end
def concat(positions) def concat(positions)
...@@ -23,9 +23,21 @@ module Gitlab ...@@ -23,9 +23,21 @@ module Gitlab
# positions (https://gitlab.com/gitlab-org/gitlab/issues/33271). # positions (https://gitlab.com/gitlab-org/gitlab/issues/33271).
def unfoldable def unfoldable
select do |position| select do |position|
position.unfoldable? && position.head_sha == @diff_head_sha position.unfoldable? && valid_head_sha?(position)
end end
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 end
end end
...@@ -23,15 +23,12 @@ module Gitlab ...@@ -23,15 +23,12 @@ module Gitlab
@parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse @parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse
end end
def render def contents
contents = @layout.render(self) # Render and remove an extra trailing new line
@contents ||= @layout.render(self).sub!(/\n(?=\Z)/, '')
write_file(contents)
end end
private def write
def write_file(contents)
filename = File.join(@output_dir, 'index.md') filename = File.join(@output_dir, 'index.md')
FileUtils.mkdir_p(@output_dir) FileUtils.mkdir_p(@output_dir)
......
...@@ -20,6 +20,3 @@ ...@@ -20,6 +20,3 @@
- type[:fields].each do |field| - type[:fields].each do |field|
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |" = "| `#{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 @@ ...@@ -3,14 +3,13 @@
module Gitlab module Gitlab
module HealthChecks module HealthChecks
module Probes module Probes
class Readiness class Collection
attr_reader :checks attr_reader :checks
# This accepts an array of objects implementing `:readiness` # This accepts an array of objects implementing `:readiness`
# that returns `::Gitlab::HealthChecks::Result` # that returns `::Gitlab::HealthChecks::Result`
def initialize(*additional_checks) def initialize(*checks)
@checks = ::Gitlab::HealthChecks::CHECKS @checks = checks
@checks += additional_checks
end end
def execute def execute
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
class BaseExporter < Daemon class BaseExporter < Daemon
attr_reader :server attr_reader :server
attr_accessor :additional_checks attr_accessor :readiness_checks
def enabled? def enabled?
settings.enabled settings.enabled
...@@ -73,11 +73,11 @@ module Gitlab ...@@ -73,11 +73,11 @@ module Gitlab
end end
def readiness_probe def readiness_probe
::Gitlab::HealthChecks::Probes::Readiness.new(*additional_checks) ::Gitlab::HealthChecks::Probes::Collection.new(*readiness_checks)
end end
def liveness_probe def liveness_probe
::Gitlab::HealthChecks::Probes::Liveness.new ::Gitlab::HealthChecks::Probes::Collection.new
end end
def render_probe(probe, req, res) def render_probe(probe, req, res)
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
def initialize def initialize
super super
self.additional_checks = [ self.readiness_checks = [
WebExporter::ExporterCheck.new(self), WebExporter::ExporterCheck.new(self),
Gitlab::HealthChecks::PumaCheck, Gitlab::HealthChecks::PumaCheck,
Gitlab::HealthChecks::UnicornCheck Gitlab::HealthChecks::UnicornCheck
......
...@@ -11,10 +11,28 @@ namespace :gitlab do ...@@ -11,10 +11,28 @@ namespace :gitlab do
task compile_docs: :environment do task compile_docs: :environment do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options) renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
renderer.render renderer.write
puts "Documentation compiled." puts "Documentation compiled."
end 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
end end
......
...@@ -3066,6 +3066,9 @@ msgstr "" ...@@ -3066,6 +3066,9 @@ msgstr ""
msgid "Choose a type..." msgid "Choose a type..."
msgstr "" msgstr ""
msgid "Choose an existing tag, or create a new one"
msgstr ""
msgid "Choose any color." msgid "Choose any color."
msgstr "" msgstr ""
...@@ -5409,6 +5412,27 @@ msgstr "" ...@@ -5409,6 +5412,27 @@ msgstr ""
msgid "Deploying to" msgid "Deploying to"
msgstr "" 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" msgid "Deprioritize label"
msgstr "" msgstr ""
...@@ -5766,6 +5790,9 @@ msgstr "" ...@@ -5766,6 +5790,9 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}" msgid "Edit Pipeline Schedule %{id}"
msgstr "" msgstr ""
msgid "Edit Release"
msgstr ""
msgid "Edit Snippet" msgid "Edit Snippet"
msgstr "" msgstr ""
...@@ -11172,6 +11199,9 @@ msgstr "" ...@@ -11172,6 +11199,9 @@ msgstr ""
msgid "Number of LOCs per commit" msgid "Number of LOCs per commit"
msgstr "" 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" msgid "Number of commits per MR"
msgstr "" msgstr ""
...@@ -13456,12 +13486,27 @@ msgstr "" ...@@ -13456,12 +13486,27 @@ msgstr ""
msgid "Release" msgid "Release"
msgstr "" msgstr ""
msgid "Release notes"
msgstr ""
msgid "Release title"
msgstr ""
msgid "Releases" msgid "Releases"
msgstr "" 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." 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 "" 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" msgid "Remember me"
msgstr "" msgstr ""
...@@ -15943,6 +15988,9 @@ msgstr "" ...@@ -15943,6 +15988,9 @@ msgstr ""
msgid "Tag list:" msgid "Tag list:"
msgstr "" msgstr ""
msgid "Tag name"
msgstr ""
msgid "Tag this commit." msgid "Tag this commit."
msgstr "" msgstr ""
...@@ -18683,6 +18731,9 @@ msgstr "" ...@@ -18683,6 +18731,9 @@ msgstr ""
msgid "Write milestone description..." msgid "Write milestone description..."
msgstr "" msgstr ""
msgid "Write your release notes or drag your files here…"
msgstr ""
msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly." msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
msgstr "" 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.
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