Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
00c78fb814
commit
8e45d25f7d
|
@ -1,3 +1,2 @@
|
|||
VERSION merge=ours
|
||||
Dangerfile gitlab-language=ruby
|
||||
db/schema.rb merge=merge_db_schema
|
||||
|
|
|
@ -67,3 +67,18 @@ docs lint:
|
|||
- bundle exec nanoc check internal_links
|
||||
# Check the internal anchor links
|
||||
- bundle exec nanoc check internal_anchors
|
||||
|
||||
graphql-docs-verify:
|
||||
extends:
|
||||
- .default-tags
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .default-only
|
||||
- .default-before_script
|
||||
- .only-graphql-changes
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
stage: test
|
||||
needs: ["setup-test-env"]
|
||||
script:
|
||||
- bundle exec rake gitlab:graphql:check_docs
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
- gitlab-org
|
||||
- docker
|
||||
|
||||
gitlab:assets:compile:
|
||||
gitlab:assets:compile pull-push-cache:
|
||||
extends: .gitlab:assets:compile-metadata
|
||||
only:
|
||||
refs:
|
||||
|
@ -63,9 +63,6 @@ gitlab:assets:compile:
|
|||
|
||||
gitlab:assets:compile pull-cache:
|
||||
extends: .gitlab:assets:compile-metadata
|
||||
except:
|
||||
refs:
|
||||
- master
|
||||
cache:
|
||||
policy: pull
|
||||
|
||||
|
@ -89,14 +86,14 @@ gitlab:assets:compile pull-cache:
|
|||
# we override the max_old_space_size to prevent OOM errors
|
||||
NODE_OPTIONS: --max_old_space_size=3584
|
||||
cache:
|
||||
key: "assets-compile:test:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v6"
|
||||
key: "assets-compile:v7"
|
||||
artifacts:
|
||||
expire_in: 7d
|
||||
paths:
|
||||
- node_modules
|
||||
- public/assets
|
||||
|
||||
compile-assets:
|
||||
compile-assets pull-push-cache:
|
||||
extends: .compile-assets-metadata
|
||||
only:
|
||||
refs:
|
||||
|
@ -104,14 +101,26 @@ compile-assets:
|
|||
cache:
|
||||
policy: pull-push
|
||||
|
||||
compile-assets pull-cache:
|
||||
extends: .compile-assets-metadata
|
||||
except:
|
||||
compile-assets pull-push-cache foss:
|
||||
extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
|
||||
only:
|
||||
refs:
|
||||
- master
|
||||
cache:
|
||||
policy: pull-push
|
||||
key: "assets-compile:v7:foss"
|
||||
|
||||
compile-assets pull-cache:
|
||||
extends: .compile-assets-metadata
|
||||
cache:
|
||||
policy: pull
|
||||
|
||||
compile-assets pull-cache foss:
|
||||
extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
|
||||
cache:
|
||||
policy: pull
|
||||
key: "assets-compile:v7:foss"
|
||||
|
||||
.only-code-frontend-job-base:
|
||||
extends:
|
||||
- .default-tags
|
||||
|
@ -121,7 +130,9 @@ compile-assets pull-cache:
|
|||
- .default-before_script
|
||||
- .only-code-changes
|
||||
- .use-pg9
|
||||
dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
|
||||
stage: test
|
||||
needs: ["setup-test-env", "compile-assets pull-cache"]
|
||||
dependencies: ["setup-test-env", "compile-assets pull-cache"]
|
||||
|
||||
.karma-base:
|
||||
extends: .only-code-frontend-job-base
|
||||
|
@ -195,6 +206,7 @@ jest-foss:
|
|||
- .default-cache
|
||||
- .default-only
|
||||
- .only-code-changes
|
||||
stage: test
|
||||
dependencies: []
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
|
@ -227,7 +239,9 @@ webpack-dev-server:
|
|||
- .default-cache
|
||||
- .default-only
|
||||
- .only-code-changes
|
||||
dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
|
||||
stage: test
|
||||
needs: ["setup-test-env", "compile-assets pull-cache"]
|
||||
dependencies: ["setup-test-env", "compile-assets pull-cache"]
|
||||
variables:
|
||||
WEBPACK_MEMORY_TEST: "true"
|
||||
script:
|
||||
|
|
|
@ -71,6 +71,12 @@
|
|||
- "doc/**/*"
|
||||
- ".markdownlint.json"
|
||||
|
||||
.only-graphql-changes:
|
||||
only:
|
||||
changes:
|
||||
- "{,ee/}app/graphql/**/*"
|
||||
- "{,ee/}lib/gitlab/graphql/**/*"
|
||||
|
||||
.only-code-qa-changes:
|
||||
only:
|
||||
changes:
|
||||
|
@ -153,4 +159,4 @@
|
|||
.only-ee-as-if-foss:
|
||||
extends: .only-ee
|
||||
variables:
|
||||
IS_GITLAB_EE: '0'
|
||||
FOSS_ONLY: '1'
|
||||
|
|
|
@ -11,7 +11,7 @@ pages:
|
|||
variables:
|
||||
- $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
|
||||
stage: pages
|
||||
dependencies: ["coverage", "karma", "gitlab:assets:compile"]
|
||||
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
|
||||
script:
|
||||
- mv public/ .public/
|
||||
- mkdir public/
|
||||
|
|
|
@ -71,4 +71,4 @@ schedule:package-and-qa:
|
|||
- .package-and-qa-base
|
||||
- .only-code-qa-changes
|
||||
- .only-canonical-schedules
|
||||
needs: ["build-qa-image", "gitlab:assets:compile"]
|
||||
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
|
||||
|
|
|
@ -53,6 +53,8 @@ setup-test-env:
|
|||
.rspec-base:
|
||||
extends: .only-code-rails-job-base
|
||||
stage: test
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
|
||||
dependencies: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
|
||||
script:
|
||||
- source scripts/rspec_helpers.sh
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~geo"
|
||||
|
@ -69,6 +71,11 @@ setup-test-env:
|
|||
reports:
|
||||
junit: junit_rspec.xml
|
||||
|
||||
.rspec-base-foss:
|
||||
extends: [".rspec-base", ".only-ee-as-if-foss"]
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache foss"]
|
||||
dependencies: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache foss"]
|
||||
|
||||
.rspec-base-pg9:
|
||||
extends:
|
||||
- .rspec-base
|
||||
|
@ -76,9 +83,8 @@ setup-test-env:
|
|||
|
||||
.rspec-base-pg9-foss:
|
||||
extends:
|
||||
- .rspec-base
|
||||
- .rspec-base-foss
|
||||
- .use-pg9
|
||||
- .only-ee-as-if-foss
|
||||
|
||||
.rspec-base-pg10:
|
||||
extends:
|
||||
|
@ -106,10 +112,9 @@ rspec system pg9:
|
|||
extends: .rspec-base-pg9
|
||||
parallel: 24
|
||||
|
||||
# TODO: This requires FOSS assets
|
||||
# rspec system pg9-foss:
|
||||
# extends: .rspec-base-pg9-foss
|
||||
# parallel: 24
|
||||
rspec system pg9-foss:
|
||||
extends: .rspec-base-pg9-foss
|
||||
parallel: 24
|
||||
|
||||
rspec unit pg10:
|
||||
extends: .rspec-base-pg10
|
||||
|
@ -229,7 +234,9 @@ rspec fast_spec_helper:
|
|||
|
||||
static-analysis:
|
||||
extends: .only-code-qa-rails-job-base
|
||||
dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
|
||||
stage: test
|
||||
needs: ["setup-test-env", "compile-assets pull-cache"]
|
||||
dependencies: ["setup-test-env", "compile-assets pull-cache"]
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
script:
|
||||
|
@ -252,16 +259,16 @@ downtime_check:
|
|||
variables:
|
||||
- $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
|
||||
stage: test
|
||||
dependencies: ["setup-test-env"]
|
||||
needs: ["setup-test-env"]
|
||||
dependencies: ["setup-test-env"]
|
||||
|
||||
.db-job-base:
|
||||
extends:
|
||||
- .only-code-rails-job-base
|
||||
- .use-pg9
|
||||
stage: test
|
||||
dependencies: ["setup-test-env"]
|
||||
needs: ["setup-test-env"]
|
||||
dependencies: ["setup-test-env"]
|
||||
|
||||
# DB migration, rollback, and seed jobs
|
||||
db:migrate:reset:
|
||||
|
|
|
@ -81,7 +81,7 @@ schedule:review-build-cng:
|
|||
extends:
|
||||
- .review-build-cng-base
|
||||
- .only-review-schedules
|
||||
needs: ["gitlab:assets:compile"]
|
||||
needs: ["gitlab:assets:compile pull-cache"]
|
||||
|
||||
.review-deploy-base:
|
||||
extends:
|
||||
|
@ -97,7 +97,7 @@ schedule:review-build-cng:
|
|||
variables:
|
||||
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
|
||||
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
|
||||
GITLAB_HELM_CHART_REF: "master"
|
||||
GITLAB_HELM_CHART_REF: "v2.3.7"
|
||||
GITLAB_EDITION: "ce"
|
||||
environment:
|
||||
name: review/${CI_COMMIT_REF_NAME}
|
||||
|
|
|
@ -36,6 +36,7 @@ const Api = {
|
|||
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
|
||||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
releasesPath: '/api/:version/projects/:id/releases',
|
||||
releasePath: '/api/:version/projects/:id/releases/:tag_name',
|
||||
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
|
||||
adminStatisticsPath: 'api/:version/application/statistics',
|
||||
|
||||
|
@ -391,6 +392,22 @@ const Api = {
|
|||
return axios.get(url);
|
||||
},
|
||||
|
||||
release(projectPath, tagName) {
|
||||
const url = Api.buildUrl(this.releasePath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':tag_name', encodeURIComponent(tagName));
|
||||
|
||||
return axios.get(url);
|
||||
},
|
||||
|
||||
updateRelease(projectPath, tagName, release) {
|
||||
const url = Api.buildUrl(this.releasePath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':tag_name', encodeURIComponent(tagName));
|
||||
|
||||
return axios.put(url, release);
|
||||
},
|
||||
|
||||
adminStatistics() {
|
||||
const url = Api.buildUrl(this.adminStatisticsPath);
|
||||
return axios.get(url);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import ZenMode from '~/zen_mode';
|
||||
import initEditRelease from '~/releases/detail';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
initEditRelease();
|
||||
});
|
|
@ -0,0 +1,156 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
|
||||
export default {
|
||||
name: 'ReleaseDetailApp',
|
||||
components: {
|
||||
GlFormInput,
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
MarkdownField,
|
||||
},
|
||||
directives: {
|
||||
autofocusonshow,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'isFetchingRelease',
|
||||
'fetchError',
|
||||
'markdownDocsPath',
|
||||
'markdownPreviewPath',
|
||||
'releasesPagePath',
|
||||
]),
|
||||
showForm() {
|
||||
return !this.isFetchingRelease && !this.fetchError;
|
||||
},
|
||||
subtitleText() {
|
||||
return sprintf(
|
||||
__(
|
||||
'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}.',
|
||||
),
|
||||
{
|
||||
codeStart: '<code>',
|
||||
codeEnd: '</code>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
tagName() {
|
||||
return this.$store.state.release.tagName;
|
||||
},
|
||||
releaseTitle: {
|
||||
get() {
|
||||
return this.$store.state.release.name;
|
||||
},
|
||||
set(title) {
|
||||
this.updateReleaseTitle(title);
|
||||
},
|
||||
},
|
||||
releaseNotes: {
|
||||
get() {
|
||||
return this.$store.state.release.description;
|
||||
},
|
||||
set(notes) {
|
||||
this.updateReleaseNotes(notes);
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchRelease();
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'fetchRelease',
|
||||
'updateRelease',
|
||||
'updateReleaseTitle',
|
||||
'updateReleaseNotes',
|
||||
'navigateToReleasesPage',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="d-flex flex-column">
|
||||
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
|
||||
<form v-if="showForm" @submit.prevent="updateRelease()">
|
||||
<div class="row">
|
||||
<gl-form-group class="col-md-6 col-lg-5 col-xl-4">
|
||||
<label for="git-ref">{{ __('Tag name') }}</label>
|
||||
<gl-form-input
|
||||
id="git-ref"
|
||||
v-model="tagName"
|
||||
type="text"
|
||||
class="form-control"
|
||||
aria-describedby="tag-name-help"
|
||||
disabled
|
||||
/>
|
||||
<div id="tag-name-help" class="form-text text-muted">
|
||||
{{ __('Choose an existing tag, or create a new one') }}
|
||||
</div>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
<gl-form-group>
|
||||
<label for="release-title">{{ __('Release title') }}</label>
|
||||
<gl-form-input
|
||||
id="release-title"
|
||||
ref="releaseTitleInput"
|
||||
v-model="releaseTitle"
|
||||
v-autofocusonshow
|
||||
autofocus
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group>
|
||||
<label for="release-notes">{{ __('Release notes') }}</label>
|
||||
<div class="bordered-box pr-3 pl-3">
|
||||
<markdown-field
|
||||
:can-attach-file="true"
|
||||
:markdown-preview-path="markdownPreviewPath"
|
||||
:markdown-docs-path="markdownDocsPath"
|
||||
:add-spacing-classes="false"
|
||||
class="prepend-top-10 append-bottom-10"
|
||||
>
|
||||
<textarea
|
||||
id="release-notes"
|
||||
slot="textarea"
|
||||
v-model="releaseNotes"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||
dir="auto"
|
||||
data-supports-quick-actions="false"
|
||||
:aria-label="__('Release notes')"
|
||||
:placeholder="__('Write your release notes or drag your files here…')"
|
||||
@keydown.meta.enter="updateRelease()"
|
||||
@keydown.ctrl.enter="updateRelease()"
|
||||
>
|
||||
</textarea>
|
||||
</markdown-field>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
|
||||
<div class="d-flex pt-3">
|
||||
<gl-button
|
||||
class="mr-auto js-submit-button"
|
||||
variant="success"
|
||||
type="submit"
|
||||
:aria-label="__('Save changes')"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
class="js-cancel-button"
|
||||
variant="default"
|
||||
type="button"
|
||||
:aria-label="__('Cancel')"
|
||||
@click="navigateToReleasesPage()"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,19 @@
|
|||
import Vue from 'vue';
|
||||
import ReleaseDetailApp from './components/app.vue';
|
||||
import createStore from './store';
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-edit-release-page');
|
||||
|
||||
const store = createStore(el.dataset);
|
||||
store.dispatch('setInitialState', el.dataset);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
components: { ReleaseDetailApp },
|
||||
render(createElement) {
|
||||
return createElement('release-detail-app');
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
import * as types from './mutation_types';
|
||||
import api from '~/api';
|
||||
import createFlash from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
|
||||
export const setInitialState = ({ commit }, initialState) =>
|
||||
commit(types.SET_INITIAL_STATE, initialState);
|
||||
|
||||
export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
|
||||
export const receiveReleaseSuccess = ({ commit }, data) =>
|
||||
commit(types.RECEIVE_RELEASE_SUCCESS, data);
|
||||
export const receiveReleaseError = ({ commit }, error) => {
|
||||
commit(types.RECEIVE_RELEASE_ERROR, error);
|
||||
createFlash(s__('Release|Something went wrong while getting the release details'));
|
||||
};
|
||||
|
||||
export const fetchRelease = ({ dispatch, state }) => {
|
||||
dispatch('requestRelease');
|
||||
|
||||
return api
|
||||
.release(state.projectId, state.tagName)
|
||||
.then(({ data: release }) => {
|
||||
const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true });
|
||||
dispatch('receiveReleaseSuccess', camelCasedRelease);
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch('receiveReleaseError', error);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
|
||||
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
|
||||
|
||||
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
|
||||
export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => {
|
||||
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
|
||||
dispatch('navigateToReleasesPage');
|
||||
};
|
||||
export const receiveUpdateReleaseError = ({ commit }, error) => {
|
||||
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
|
||||
createFlash(s__('Release|Something went wrong while saving the release details'));
|
||||
};
|
||||
|
||||
export const updateRelease = ({ dispatch, state }) => {
|
||||
dispatch('requestUpdateRelease');
|
||||
|
||||
return api
|
||||
.updateRelease(state.projectId, state.tagName, {
|
||||
name: state.release.name,
|
||||
description: state.release.description,
|
||||
})
|
||||
.then(() => dispatch('receiveUpdateReleaseSuccess'))
|
||||
.catch(error => {
|
||||
dispatch('receiveUpdateReleaseError', error);
|
||||
});
|
||||
};
|
||||
|
||||
export const navigateToReleasesPage = ({ state }) => {
|
||||
redirectTo(state.releasesPagePath);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import * as actions from './actions';
|
||||
import mutations from './mutations';
|
||||
import state from './state';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default () =>
|
||||
new Vuex.Store({
|
||||
actions,
|
||||
mutations,
|
||||
state,
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
|
||||
|
||||
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
|
||||
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
|
||||
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
|
||||
|
||||
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
|
||||
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
|
||||
|
||||
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
|
||||
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
|
||||
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
|
|
@ -0,0 +1,42 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_STATE](state, initialState) {
|
||||
Object.keys(state).forEach(key => {
|
||||
state[key] = initialState[key];
|
||||
});
|
||||
},
|
||||
|
||||
[types.REQUEST_RELEASE](state) {
|
||||
state.isFetchingRelease = true;
|
||||
},
|
||||
[types.RECEIVE_RELEASE_SUCCESS](state, data) {
|
||||
state.fetchError = undefined;
|
||||
state.isFetchingRelease = false;
|
||||
state.release = data;
|
||||
},
|
||||
[types.RECEIVE_RELEASE_ERROR](state, error) {
|
||||
state.fetchError = error;
|
||||
state.isFetchingRelease = false;
|
||||
state.release = undefined;
|
||||
},
|
||||
|
||||
[types.UPDATE_RELEASE_TITLE](state, title) {
|
||||
state.release.name = title;
|
||||
},
|
||||
[types.UPDATE_RELEASE_NOTES](state, notes) {
|
||||
state.release.description = notes;
|
||||
},
|
||||
|
||||
[types.REQUEST_UPDATE_RELEASE](state) {
|
||||
state.isUpdatingRelease = true;
|
||||
},
|
||||
[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) {
|
||||
state.updateError = undefined;
|
||||
state.isUpdatingRelease = false;
|
||||
},
|
||||
[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) {
|
||||
state.updateError = error;
|
||||
state.isUpdatingRelease = false;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
export default () => ({
|
||||
projectId: null,
|
||||
tagName: null,
|
||||
releasesPagePath: null,
|
||||
markdownDocsPath: null,
|
||||
markdownPreviewPath: null,
|
||||
|
||||
release: null,
|
||||
|
||||
isFetchingRelease: false,
|
||||
fetchError: null,
|
||||
|
||||
isUpdatingRelease: false,
|
||||
updateError: null,
|
||||
});
|
|
@ -123,7 +123,7 @@ ul.content-list {
|
|||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
a:not(.default-link-color) {
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.tag-release-link {
|
||||
color: $blue-600 !important;
|
||||
}
|
|
@ -4,18 +4,31 @@ class HealthController < ActionController::Base
|
|||
protect_from_forgery with: :exception, prepend: true
|
||||
include RequiresWhitelistedMonitoringClient
|
||||
|
||||
CHECKS = [
|
||||
Gitlab::HealthChecks::DbCheck,
|
||||
Gitlab::HealthChecks::Redis::RedisCheck,
|
||||
Gitlab::HealthChecks::Redis::CacheCheck,
|
||||
Gitlab::HealthChecks::Redis::QueuesCheck,
|
||||
Gitlab::HealthChecks::Redis::SharedStateCheck,
|
||||
Gitlab::HealthChecks::GitalyCheck
|
||||
].freeze
|
||||
|
||||
def readiness
|
||||
render_probe(::Gitlab::HealthChecks::Probes::Readiness)
|
||||
# readiness check is a collection with all above application-level checks
|
||||
render_checks(*CHECKS)
|
||||
end
|
||||
|
||||
def liveness
|
||||
render_probe(::Gitlab::HealthChecks::Probes::Liveness)
|
||||
# liveness check is a collection without additional checks
|
||||
render_checks
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_probe(probe_class)
|
||||
result = probe_class.new.execute
|
||||
def render_checks(*checks)
|
||||
result = Gitlab::HealthChecks::Probes::Collection
|
||||
.new(*checks)
|
||||
.execute
|
||||
|
||||
# disable static error pages at the gitlab-workhorse level, we want to see this error response even in production
|
||||
headers["X-GitLab-Custom-Error"] = 1 unless result.success?
|
||||
|
|
|
@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController
|
|||
@deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def deployment
|
||||
@deployment ||= environment.deployments.find_by(iid: params[:id])
|
||||
@deployment ||= environment.deployments.find_successful_deployment!(params[:id])
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def environment
|
||||
@environment ||= project.environments.find(params[:environment_id])
|
||||
|
|
|
@ -51,9 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
def render_diffs
|
||||
@environment = @merge_request.environments_for(current_user).last
|
||||
|
||||
note_positions = renderable_notes.map(&:position).compact
|
||||
@diffs.unfold_diff_files(note_positions)
|
||||
|
||||
@diffs.unfold_diff_files(note_positions.unfoldable)
|
||||
@diffs.write_cache
|
||||
|
||||
request = {
|
||||
|
@ -140,6 +138,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
|
||||
end
|
||||
|
||||
def note_positions
|
||||
@note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
|
||||
end
|
||||
|
||||
def renderable_notes
|
||||
define_diff_comment_vars unless @notes
|
||||
|
||||
|
|
|
@ -289,7 +289,8 @@ module ApplicationSettingsHelper
|
|||
:snowplow_collector_hostname,
|
||||
:snowplow_cookie_domain,
|
||||
:snowplow_enabled,
|
||||
:snowplow_site_id
|
||||
:snowplow_site_id,
|
||||
:push_event_hooks_limit
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -18,12 +18,16 @@ module EnvironmentHelper
|
|||
end
|
||||
end
|
||||
|
||||
def deployment_path(deployment)
|
||||
[deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
|
||||
end
|
||||
|
||||
def deployment_link(deployment, text: nil)
|
||||
return unless deployment
|
||||
|
||||
link_label = text ? text : "##{deployment.iid}"
|
||||
|
||||
link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
|
||||
link_to link_label, deployment_path(deployment)
|
||||
end
|
||||
|
||||
def last_deployment_link_for_environment_build(project, build)
|
||||
|
@ -32,4 +36,31 @@ module EnvironmentHelper
|
|||
|
||||
deployment_link(environment.last_deployment)
|
||||
end
|
||||
|
||||
def render_deployment_status(deployment)
|
||||
status = deployment.status
|
||||
|
||||
status_text =
|
||||
case status
|
||||
when 'created'
|
||||
s_('Deployment|created')
|
||||
when 'running'
|
||||
s_('Deployment|running')
|
||||
when 'success'
|
||||
s_('Deployment|success')
|
||||
when 'failed'
|
||||
s_('Deployment|failed')
|
||||
when 'canceled'
|
||||
s_('Deployment|canceled')
|
||||
end
|
||||
|
||||
klass = "ci-status ci-#{status.dasherize}"
|
||||
text = "#{ci_icon_for_status(status)} #{status_text}".html_safe
|
||||
|
||||
if deployment.deployable
|
||||
link_to(text, deployment_path(deployment), class: klass)
|
||||
else
|
||||
content_tag(:span, text, class: klass)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,4 +19,14 @@ module ReleasesHelper
|
|||
documentation_path: help_page
|
||||
}
|
||||
end
|
||||
|
||||
def data_for_edit_release_page
|
||||
{
|
||||
project_id: @project.id,
|
||||
tag_name: @release.tag,
|
||||
markdown_preview_path: preview_markdown_path(@project),
|
||||
markdown_docs_path: help_page_path('user/markdown'),
|
||||
releases_page_path: project_releases_path(@project, anchor: @release.tag)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -214,6 +214,9 @@ class ApplicationSetting < ApplicationRecord
|
|||
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
|
||||
allow_nil: false
|
||||
|
||||
validates :push_event_hooks_limit,
|
||||
numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
SUPPORTED_KEY_TYPES.each do |type|
|
||||
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
||||
end
|
||||
|
|
|
@ -82,6 +82,7 @@ module ApplicationSettingImplementation
|
|||
polling_interval_multiplier: 1,
|
||||
project_export_enabled: true,
|
||||
protected_ci_variables: false,
|
||||
push_event_hooks_limit: 3,
|
||||
raw_blob_request_limit: 300,
|
||||
recaptcha_enabled: false,
|
||||
login_recaptcha_protection_enabled: false,
|
||||
|
|
|
@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
|
|||
belongs_to :environment, required: true
|
||||
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
|
||||
belongs_to :user
|
||||
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
|
||||
belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
|
||||
|
||||
has_internal_id :iid, scope: :project, init: ->(s) do
|
||||
Deployment.where(project: s.project).maximum(:iid) if s&.project
|
||||
|
@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
|
|||
|
||||
scope :for_environment, -> (environment) { where(environment_id: environment) }
|
||||
|
||||
scope :visible, -> { where(status: %i[running success failed canceled]) }
|
||||
|
||||
state_machine :status, initial: :created do
|
||||
event :run do
|
||||
transition created: :running
|
||||
|
@ -73,6 +75,10 @@ class Deployment < ApplicationRecord
|
|||
find(ids)
|
||||
end
|
||||
|
||||
def self.find_successful_deployment!(iid)
|
||||
success.find_by!(iid: iid)
|
||||
end
|
||||
|
||||
def commit
|
||||
project.commit(sha)
|
||||
end
|
||||
|
|
|
@ -6,7 +6,8 @@ class Environment < ApplicationRecord
|
|||
|
||||
belongs_to :project, required: true
|
||||
|
||||
has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
|
||||
|
||||
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
|
||||
|
||||
|
@ -81,6 +82,10 @@ class Environment < ApplicationRecord
|
|||
pluck(:name)
|
||||
end
|
||||
|
||||
def self.find_or_create_by_name(name)
|
||||
find_or_create_by(name: name)
|
||||
end
|
||||
|
||||
def predefined_variables
|
||||
Gitlab::Ci::Variables::Collection.new
|
||||
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
|
||||
|
|
|
@ -281,7 +281,7 @@ class Project < ApplicationRecord
|
|||
has_many :variables, class_name: 'Ci::Variable'
|
||||
has_many :triggers, class_name: 'Ci::Trigger'
|
||||
has_many :environments
|
||||
has_many :deployments, -> { success }
|
||||
has_many :deployments
|
||||
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
|
||||
has_many :project_deploy_tokens
|
||||
has_many :deploy_tokens, through: :project_deploy_tokens
|
||||
|
|
|
@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
|
|||
can?(:update_build, @subject.deployable)
|
||||
end
|
||||
|
||||
rule { ~can_retry_deployable }.policy do
|
||||
condition(:has_deployable) do
|
||||
@subject.deployable.present?
|
||||
end
|
||||
|
||||
condition(:can_update_deployment) do
|
||||
can?(:update_deployment, @subject.environment)
|
||||
end
|
||||
|
||||
rule { has_deployable & ~can_retry_deployable }.policy do
|
||||
prevent :create_deployment
|
||||
prevent :update_deployment
|
||||
end
|
||||
|
||||
rule { ~can_update_deployment }.policy do
|
||||
prevent :update_deployment
|
||||
end
|
||||
end
|
||||
|
|
|
@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :destroy_container_image
|
||||
enable :create_environment
|
||||
enable :create_deployment
|
||||
enable :update_deployment
|
||||
enable :create_release
|
||||
enable :update_release
|
||||
end
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Deployments
|
||||
class AfterCreateService
|
||||
attr_reader :deployment
|
||||
attr_reader :deployable
|
||||
|
||||
delegate :environment, to: :deployment
|
||||
delegate :variables, to: :deployable
|
||||
delegate :options, to: :deployable, allow_nil: true
|
||||
|
||||
def initialize(deployment)
|
||||
@deployment = deployment
|
||||
@deployable = deployment.deployable
|
||||
end
|
||||
|
||||
def execute
|
||||
deployment.create_ref
|
||||
deployment.invalidate_cache
|
||||
|
||||
update_environment(deployment)
|
||||
|
||||
deployment
|
||||
end
|
||||
|
||||
def update_environment(deployment)
|
||||
ActiveRecord::Base.transaction do
|
||||
if (url = expanded_environment_url)
|
||||
environment.external_url = url
|
||||
end
|
||||
|
||||
environment.fire_state_event(action)
|
||||
|
||||
if environment.save && !environment.stopped?
|
||||
deployment.update_merge_request_metrics!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def environment_options
|
||||
options&.dig(:environment) || {}
|
||||
end
|
||||
|
||||
def expanded_environment_url
|
||||
ExpandVariables.expand(environment_url, -> { variables }) if environment_url
|
||||
end
|
||||
|
||||
def environment_url
|
||||
environment_options[:url]
|
||||
end
|
||||
|
||||
def action
|
||||
environment_options[:action] || 'start'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Deployments
|
||||
class CreateService
|
||||
attr_reader :environment, :current_user, :params
|
||||
|
||||
def initialize(environment, current_user, params)
|
||||
@environment = environment
|
||||
@current_user = current_user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
create_deployment.tap do |deployment|
|
||||
AfterCreateService.new(deployment).execute if deployment.persisted?
|
||||
end
|
||||
end
|
||||
|
||||
def create_deployment
|
||||
environment.deployments.create(deployment_attributes)
|
||||
end
|
||||
|
||||
def deployment_attributes
|
||||
# We use explicit parameters here so we never by accident allow parameters
|
||||
# to be set that one should not be able to set (e.g. the row ID).
|
||||
{
|
||||
cluster_id: environment.deployment_platform&.cluster_id,
|
||||
project_id: environment.project_id,
|
||||
environment_id: environment.id,
|
||||
ref: params[:ref],
|
||||
tag: params[:tag],
|
||||
sha: params[:sha],
|
||||
user: current_user,
|
||||
on_stop: params[:on_stop],
|
||||
status: params[:status]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Deployments
|
||||
class UpdateService
|
||||
attr_reader :deployment, :params
|
||||
|
||||
def initialize(deployment, params)
|
||||
@deployment = deployment
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
deployment.update(status: params[:status])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -62,6 +62,8 @@ module Git
|
|||
end
|
||||
|
||||
def execute_project_hooks
|
||||
return unless params.fetch(:execute_project_hooks, true)
|
||||
|
||||
# Creating push_data invokes one CommitDelta RPC per commit. Only
|
||||
# build this data if we actually need it.
|
||||
project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
|
||||
|
|
|
@ -17,7 +17,7 @@ module Git
|
|||
changes_by_action = group_changes_by_action(changes)
|
||||
|
||||
changes_by_action.each do |_, changes|
|
||||
process_changes(ref_type, changes) if changes.any?
|
||||
process_changes(ref_type, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,7 +34,11 @@ module Git
|
|||
:pushed
|
||||
end
|
||||
|
||||
def process_changes(ref_type, changes)
|
||||
def execute_project_hooks?(changes)
|
||||
(changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project)
|
||||
end
|
||||
|
||||
def process_changes(ref_type, changes, execute_project_hooks:)
|
||||
push_service_class = push_service_class_for(ref_type)
|
||||
|
||||
changes.each do |change|
|
||||
|
@ -43,7 +47,8 @@ module Git
|
|||
current_user,
|
||||
change: change,
|
||||
push_options: params[:push_options],
|
||||
create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
|
||||
create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
|
||||
execute_project_hooks: execute_project_hooks
|
||||
).execute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateDeploymentService
|
||||
attr_reader :deployment
|
||||
attr_reader :deployable
|
||||
|
||||
delegate :environment, to: :deployment
|
||||
delegate :variables, to: :deployable
|
||||
|
||||
def initialize(deployment)
|
||||
@deployment = deployment
|
||||
@deployable = deployment.deployable
|
||||
end
|
||||
|
||||
def execute
|
||||
deployment.create_ref
|
||||
deployment.invalidate_cache
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
environment.external_url = expanded_environment_url if
|
||||
expanded_environment_url
|
||||
|
||||
environment.fire_state_event(action)
|
||||
|
||||
break unless environment.save
|
||||
break if environment.stopped?
|
||||
|
||||
deployment.tap(&:update_merge_request_metrics!)
|
||||
end
|
||||
|
||||
deployment
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def environment_options
|
||||
@environment_options ||= deployable.options&.dig(:environment) || {}
|
||||
end
|
||||
|
||||
def expanded_environment_url
|
||||
return @expanded_environment_url if defined?(@expanded_environment_url)
|
||||
return unless environment_url
|
||||
|
||||
@expanded_environment_url =
|
||||
ExpandVariables.expand(environment_url, -> { variables })
|
||||
end
|
||||
|
||||
def environment_url
|
||||
environment_options[:url]
|
||||
end
|
||||
|
||||
def action
|
||||
environment_options[:action] || 'start'
|
||||
end
|
||||
end
|
||||
|
||||
UpdateDeploymentService.prepend_if_ee('EE::UpdateDeploymentService')
|
|
@ -20,5 +20,10 @@
|
|||
= f.number_field :raw_blob_request_limit, class: 'form-control'
|
||||
.form-text.text-muted
|
||||
= _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.')
|
||||
.form-group
|
||||
= f.label :push_event_hooks_limit, class: 'label-bold'
|
||||
= f.number_field :push_event_hooks_limit, class: 'form-control'
|
||||
.form-text.text-muted
|
||||
= _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
|
||||
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- breadcrumb_title "Repository"
|
||||
- page_title @blob.path, @ref
|
||||
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit)
|
||||
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
|
||||
|
||||
.js-signature-container{ data: { 'signatures-path': signatures_path } }
|
||||
|
||||
|
|
|
@ -1,31 +1,49 @@
|
|||
.gl-responsive-table-row.deployment{ role: 'row' }
|
||||
.table-section.section-15{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Status")
|
||||
.table-mobile-content
|
||||
= render_deployment_status(deployment)
|
||||
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("ID")
|
||||
%strong.table-mobile-content ##{deployment.iid}
|
||||
|
||||
.table-section.section-30{ role: 'gridcell' }
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Triggerer")
|
||||
.table-mobile-content
|
||||
- if deployment.deployed_by
|
||||
= user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
|
||||
|
||||
.table-section.section-25{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Commit")
|
||||
= render 'projects/deployments/commit', deployment: deployment
|
||||
|
||||
.table-section.section-25.build-column{ role: 'gridcell' }
|
||||
.table-section.section-10.build-column{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Job")
|
||||
- if deployment.deployable
|
||||
.table-mobile-content
|
||||
.flex-truncate-parent
|
||||
.flex-truncate-child
|
||||
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
|
||||
= link_to deployment_path(deployment), class: 'build-link' do
|
||||
#{deployment.deployable.name} (##{deployment.deployable.id})
|
||||
- if deployment.deployed_by
|
||||
%div
|
||||
by
|
||||
= user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
|
||||
- else
|
||||
.badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
|
||||
= s_('Deployment|API')
|
||||
|
||||
.table-section.section-15{ role: 'gridcell' }
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Created")
|
||||
- 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.created_at)
|
||||
|
||||
.table-section.section-20.table-button-footer{ role: 'gridcell' }
|
||||
.table-section.section-10{ role: 'gridcell' }
|
||||
.table-mobile-header{ role: 'rowheader' }= _("Deployed")
|
||||
- if deployment.deployed_at
|
||||
%span.table-mobile-content.flex-truncate-parent
|
||||
%span.flex-truncate-child
|
||||
= time_ago_with_tooltip(deployment.deployed_at)
|
||||
|
||||
.table-section.section-10.table-button-footer{ role: 'gridcell' }
|
||||
.btn-group.table-action-buttons
|
||||
= render 'projects/deployments/actions', deployment: deployment
|
||||
= render 'projects/deployments/rollback', deployment: deployment
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- if can?(current_user, :create_deployment, deployment)
|
||||
- if deployment.deployable && can?(current_user, :create_deployment, deployment)
|
||||
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
|
||||
= button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
|
||||
- if deployment.last?
|
||||
|
|
|
@ -60,10 +60,13 @@
|
|||
.table-holder
|
||||
.ci-table.environments{ role: 'grid' }
|
||||
.gl-responsive-table-row.table-row-header{ role: 'row' }
|
||||
.table-section.section-15{ role: 'columnheader' }= _('Status')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('ID')
|
||||
.table-section.section-30{ role: 'columnheader' }= _('Commit')
|
||||
.table-section.section-25{ role: 'columnheader' }= _('Job')
|
||||
.table-section.section-15{ role: 'columnheader' }= _('Created')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Triggerer')
|
||||
.table-section.section-25{ role: 'columnheader' }= _('Commit')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Job')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Created')
|
||||
.table-section.section-10{ role: 'columnheader' }= _('Deployed')
|
||||
|
||||
= render @deployments
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
|
||||
data: { toggle: 'modal', target: '.issues-import-modal' } }
|
||||
- if type == :icon
|
||||
= sprite_icon('upload')
|
||||
= sprite_icon('import')
|
||||
- else
|
||||
= _('Import CSV')
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
- page_title _('Edit Release')
|
||||
|
||||
#js-edit-release-page{ data: data_for_edit_release_page }
|
|
@ -24,7 +24,7 @@
|
|||
.text-secondary
|
||||
= icon('rocket')
|
||||
= _("Release")
|
||||
= link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'default-link-color'
|
||||
= link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'tag-release-link'
|
||||
- if release.description.present?
|
||||
.description.md.prepend-top-default
|
||||
= markdown_field(release, :description)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
|
||||
= icon('rss')
|
||||
= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
|
||||
= sprite_icon('rss')
|
||||
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
|
||||
= custom_icon('icon_calendar')
|
||||
= sprite_icon('calendar')
|
||||
|
|
|
@ -10,7 +10,7 @@ module Deployments
|
|||
Deployment.find_by_id(deployment_id).try do |deployment|
|
||||
break unless deployment.success?
|
||||
|
||||
UpdateDeploymentService.new(deployment).execute
|
||||
Deployments::AfterCreateService.new(deployment).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix showing diff when it has legacy diff notes
|
||||
merge_request: 18510
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Don't execute webhooks/services when above limit
|
||||
merge_request: 17874
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use correct icons for issue actions
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Introduce new Ansi2json parser to convert job logs to JSON
|
||||
merge_request: 18133
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add individual inherited member lookup API
|
||||
merge_request: 17744
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add API for manually creating and updating deployments
|
||||
merge_request: 17620
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add "Edit Release" page
|
||||
merge_request: 18033
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix button link foreground color
|
||||
merge_request: 18669
|
||||
author:
|
||||
type: fixed
|
|
@ -3,12 +3,12 @@ const path = require('path');
|
|||
|
||||
const ROOT_PATH = path.resolve(__dirname, '../..');
|
||||
|
||||
// The `IS_GITLAB_EE` is always `string` or `nil`
|
||||
// The `FOSS_ONLY` is always `string` or `nil`
|
||||
// Thus the nil or empty string will result
|
||||
// in using default value: true
|
||||
// in using default value: false
|
||||
//
|
||||
// The behavior needs to be synchronised with
|
||||
// lib/gitlab.rb: Gitlab.ee?
|
||||
const isFossOnly = JSON.parse(process.env.FOSS_ONLY || 'false');
|
||||
module.exports =
|
||||
fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) &&
|
||||
(!process.env.IS_GITLAB_EE || JSON.parse(process.env.IS_GITLAB_EE));
|
||||
fs.existsSync(path.join(ROOT_PATH, 'ee', 'app', 'models', 'license.rb')) && !isFossOnly;
|
||||
|
|
|
@ -380,7 +380,7 @@ module.exports = {
|
|||
|
||||
new webpack.DefinePlugin({
|
||||
// This one is used to define window.gon.ee and other things properly in tests:
|
||||
'process.env.IS_GITLAB_EE': JSON.stringify(IS_EE),
|
||||
'process.env.IS_EE': JSON.stringify(IS_EE),
|
||||
// This one is used to check against "EE" properly in application code
|
||||
IS_EE: IS_EE ? 'window.gon && window.gon.ee' : JSON.stringify(false),
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPushEventHooksLimitToApplicationSettings < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_column_with_default(:application_settings, :push_event_hooks_limit, :integer, default: 3)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column(:application_settings, :push_event_hooks_limit)
|
||||
end
|
||||
end
|
|
@ -338,6 +338,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
|
|||
t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
|
||||
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
|
||||
t.integer "throttle_incident_management_notification_per_period", default: 3600
|
||||
t.integer "push_event_hooks_limit", default: 3, null: false
|
||||
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
|
||||
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
|
||||
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
|
||||
|
|
|
@ -217,14 +217,19 @@ workload. Your workload is influenced by factors such as - but not limited to -
|
|||
how active your users are, how much automation you use, mirroring, and
|
||||
repo/change size.
|
||||
|
||||
- 3 PostgreSQL - 4 CPU, 16GiB memory per node
|
||||
- 1 PgBouncer - 2 CPU, 4GiB memory
|
||||
- 2 Redis - 2 CPU, 8GiB memory per node
|
||||
- 3 Consul/Sentinel - 2 CPU, 2GiB memory per node
|
||||
- 4 Sidekiq - 4 CPU, 16GiB memory per node
|
||||
- 5 GitLab application nodes - 16 CPU, 64GiB memory per node
|
||||
- 1 Gitaly - 16 CPU, 64GiB memory
|
||||
- 1 Monitoring node - 2 CPU, 8GiB memory, 100GiB local storage
|
||||
| Service | Configuration | GCP type |
|
||||
| ------------------------------|-------------------------|----------------|
|
||||
| 3 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
|
||||
| 3 PostgreSQL | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 1 PgBouncer | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
| X Gitaly[^1] <br> - Gitaly Ruby workers on each node set to 90% of available CPUs with 16 threads | 16 vCPU, 60GB Memory | n1-standard-16 |
|
||||
| 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
|
||||
| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
|
||||
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
|
||||
### 25,000 User Configuration
|
||||
|
||||
|
@ -249,7 +254,7 @@ adjusted prior to certification based on performance testing.
|
|||
| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
| 1 NFS Server | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
|
||||
| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
|
||||
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
|
||||
|
@ -277,15 +282,15 @@ testing.
|
|||
| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
|
||||
| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
| 1 NFS Server | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
|
||||
| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
|
||||
| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
|
||||
|
||||
[^1]: Gitaly node requirements are dependent on customer data. We recommend 2
|
||||
nodes as an absolute minimum for performance at the 25,000 user scale and
|
||||
4 nodes as an absolute minimum at the 50,000 user scale, but additional
|
||||
nodes should be considered in conjunction with a review of project counts
|
||||
and sizes.
|
||||
nodes as an absolute minimum for performance at the 10,000 and 25,000 user
|
||||
scale and 4 nodes as an absolute minimum at the 50,000 user scale, but
|
||||
additional nodes should be considered in conjunction with a review of
|
||||
project counts and sizes.
|
||||
|
||||
[^2]: HAProxy is the only tested and recommended load balancer. Additional
|
||||
options may be supported in the future.
|
||||
|
|
|
@ -223,3 +223,100 @@ Example of response
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Create a deployment
|
||||
|
||||
```
|
||||
POST /projects/:id/deployments
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|---------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `environment` | string | yes | The name of the environment to create the deployment for |
|
||||
| `sha` | string | yes | The SHA of the commit that is deployed |
|
||||
| `ref` | string | yes | The name of the branch or tag that is deployed |
|
||||
| `tag` | boolean | yes | A boolean that indicates if the deployed ref is a tag (true) or not (false) |
|
||||
| `status` | string | yes | The status of the deployment |
|
||||
|
||||
The status can be one of the following values:
|
||||
|
||||
- created
|
||||
- running
|
||||
- success
|
||||
- failed
|
||||
- canceled
|
||||
|
||||
```bash
|
||||
curl --data "environment=production&sha=a91957a858320c0e17f3a0eca7cfacbff50ea29a&ref=master&tag=false&status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments"
|
||||
```
|
||||
|
||||
Example of a response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"iid": 2,
|
||||
"ref": "master",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"created_at": "2016-08-11T11:32:35.444Z",
|
||||
"status": "success",
|
||||
"user": {
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"environment": {
|
||||
"id": 9,
|
||||
"name": "production",
|
||||
"external_url": "https://about.gitlab.com"
|
||||
},
|
||||
"deployable": null
|
||||
}
|
||||
```
|
||||
|
||||
## Updating a deployment
|
||||
|
||||
```
|
||||
PUT /projects/:id/deployments/:deployment_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|------------------|----------------|----------|---------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `deployment_id` | integer | yes | The ID of the deployment to update |
|
||||
| `status` | string | yes | The new status of the deployment |
|
||||
|
||||
```bash
|
||||
curl --request PUT --data "status=success" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/42"
|
||||
```
|
||||
|
||||
Example of a response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"iid": 2,
|
||||
"ref": "master",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"created_at": "2016-08-11T11:32:35.444Z",
|
||||
"status": "success",
|
||||
"user": {
|
||||
"name": "Administrator",
|
||||
"username": "root",
|
||||
"id": 1,
|
||||
"state": "active",
|
||||
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
|
||||
"web_url": "http://localhost:3000/root"
|
||||
},
|
||||
"environment": {
|
||||
"id": 9,
|
||||
"name": "production",
|
||||
"external_url": "https://about.gitlab.com"
|
||||
},
|
||||
"deployable": null
|
||||
}
|
||||
```
|
||||
|
|
|
@ -54,9 +54,87 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `message` | String | |
|
||||
| `authoredDate` | Time | |
|
||||
| `webUrl` | String! | |
|
||||
| `signatureHtml` | String | Rendered html for the commit signature |
|
||||
| `author` | User | |
|
||||
| `latestPipeline` | Pipeline | Latest pipeline for this commit |
|
||||
|
||||
### CreateDiffNotePayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `note` | Note | The note after mutation |
|
||||
|
||||
### CreateImageDiffNotePayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `note` | Note | The note after mutation |
|
||||
|
||||
### CreateNotePayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `note` | Note | The note after mutation |
|
||||
|
||||
### Design
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `id` | ID! | |
|
||||
| `project` | Project! | |
|
||||
| `issue` | Issue! | |
|
||||
| `notesCount` | Int! | The total count of user-created notes for this design |
|
||||
| `filename` | String! | |
|
||||
| `fullPath` | String! | |
|
||||
| `event` | DesignVersionEvent! | The change that happened to the design at this version |
|
||||
| `image` | String! | |
|
||||
| `diffRefs` | DiffRefs! | |
|
||||
|
||||
### DesignCollection
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `project` | Project! | |
|
||||
| `issue` | Issue! | |
|
||||
|
||||
### DesignManagementDeletePayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `version` | DesignVersion | The new version in which the designs are deleted |
|
||||
|
||||
### DesignManagementUploadPayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `designs` | Design! => Array | The designs that were uploaded by the mutation |
|
||||
| `skippedDesigns` | Design! => Array | Any designs that were skipped from the upload due to there being no change to their content since their last version |
|
||||
|
||||
### DesignVersion
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `id` | ID! | |
|
||||
| `sha` | ID! | |
|
||||
|
||||
### DestroyNotePayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `note` | Note | The note after mutation |
|
||||
|
||||
### DetailedStatus
|
||||
|
||||
| Name | Type | Description |
|
||||
|
@ -74,9 +152,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `headSha` | String! | The sha of the head at the time the comment was made |
|
||||
| `baseSha` | String | The merge base of the branch the comment was made on |
|
||||
| `startSha` | String! | The sha of the branch being compared against |
|
||||
| `diffRefs` | DiffRefs! | |
|
||||
| `filePath` | String! | The path of the file that was changed |
|
||||
| `oldPath` | String | The path of the file on the start sha. |
|
||||
| `newPath` | String | The path of the file on the head sha. |
|
||||
|
@ -88,13 +164,146 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `width` | Int | The total width of the image |
|
||||
| `height` | Int | The total height of the image |
|
||||
|
||||
### DiffRefs
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `headSha` | String! | The sha of the head at the time the comment was made |
|
||||
| `baseSha` | String! | The merge base of the branch the comment was made on |
|
||||
| `startSha` | String! | The sha of the branch being compared against |
|
||||
|
||||
### Discussion
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `id` | ID! | |
|
||||
| `replyId` | ID! | The ID used to reply to this discussion |
|
||||
| `createdAt` | Time! | |
|
||||
|
||||
### Epic
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `userPermissions` | EpicPermissions! | Permissions for the current user on the resource |
|
||||
| `id` | ID! | |
|
||||
| `iid` | ID! | |
|
||||
| `title` | String | |
|
||||
| `description` | String | |
|
||||
| `state` | EpicState! | |
|
||||
| `group` | Group! | |
|
||||
| `parent` | Epic | |
|
||||
| `author` | User! | |
|
||||
| `startDate` | Time | |
|
||||
| `startDateIsFixed` | Boolean | |
|
||||
| `startDateFixed` | Time | |
|
||||
| `startDateFromMilestones` | Time | |
|
||||
| `dueDate` | Time | |
|
||||
| `dueDateIsFixed` | Boolean | |
|
||||
| `dueDateFixed` | Time | |
|
||||
| `dueDateFromMilestones` | Time | |
|
||||
| `closedAt` | Time | |
|
||||
| `createdAt` | Time | |
|
||||
| `updatedAt` | Time | |
|
||||
| `hasChildren` | Boolean! | |
|
||||
| `hasIssues` | Boolean! | |
|
||||
| `webPath` | String! | |
|
||||
| `webUrl` | String! | |
|
||||
| `relativePosition` | Int | The relative position of the epic in the Epic tree |
|
||||
| `relationPath` | String | |
|
||||
| `reference` | String! | |
|
||||
|
||||
### EpicIssue
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
|
||||
| `iid` | ID! | |
|
||||
| `title` | String! | |
|
||||
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
|
||||
| `description` | String | |
|
||||
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
|
||||
| `state` | IssueState! | |
|
||||
| `reference` | String! | |
|
||||
| `author` | User! | |
|
||||
| `milestone` | Milestone | |
|
||||
| `dueDate` | Time | |
|
||||
| `confidential` | Boolean! | |
|
||||
| `discussionLocked` | Boolean! | |
|
||||
| `upvotes` | Int! | |
|
||||
| `downvotes` | Int! | |
|
||||
| `userNotesCount` | Int! | |
|
||||
| `webPath` | String! | |
|
||||
| `webUrl` | String! | |
|
||||
| `relativePosition` | Int | |
|
||||
| `timeEstimate` | Int! | The time estimate on the issue |
|
||||
| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
|
||||
| `closedAt` | Time | |
|
||||
| `createdAt` | Time! | |
|
||||
| `updatedAt` | Time! | |
|
||||
| `taskCompletionStatus` | TaskCompletionStatus! | |
|
||||
| `epic` | Epic | The epic to which issue belongs |
|
||||
| `weight` | Int | |
|
||||
| `designs` | DesignCollection | |
|
||||
| `designCollection` | DesignCollection | |
|
||||
| `epicIssueId` | ID! | |
|
||||
| `relationPath` | String | |
|
||||
| `id` | ID | The global id of the epic-issue relation |
|
||||
|
||||
### EpicPermissions
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `readEpic` | Boolean! | Whether or not a user can perform `read_epic` on this resource |
|
||||
| `readEpicIid` | Boolean! | Whether or not a user can perform `read_epic_iid` on this resource |
|
||||
| `updateEpic` | Boolean! | Whether or not a user can perform `update_epic` on this resource |
|
||||
| `destroyEpic` | Boolean! | Whether or not a user can perform `destroy_epic` on this resource |
|
||||
| `adminEpic` | Boolean! | Whether or not a user can perform `admin_epic` on this resource |
|
||||
| `createEpic` | Boolean! | Whether or not a user can perform `create_epic` on this resource |
|
||||
| `createNote` | Boolean! | Whether or not a user can perform `create_note` on this resource |
|
||||
| `awardEmoji` | Boolean! | Whether or not a user can perform `award_emoji` on this resource |
|
||||
|
||||
### EpicTreeReorderPayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
|
||||
### ExtendedIssue
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
|
||||
| `iid` | ID! | |
|
||||
| `title` | String! | |
|
||||
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
|
||||
| `description` | String | |
|
||||
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
|
||||
| `state` | IssueState! | |
|
||||
| `reference` | String! | |
|
||||
| `author` | User! | |
|
||||
| `milestone` | Milestone | |
|
||||
| `dueDate` | Time | |
|
||||
| `confidential` | Boolean! | |
|
||||
| `discussionLocked` | Boolean! | |
|
||||
| `upvotes` | Int! | |
|
||||
| `downvotes` | Int! | |
|
||||
| `userNotesCount` | Int! | |
|
||||
| `webPath` | String! | |
|
||||
| `webUrl` | String! | |
|
||||
| `relativePosition` | Int | |
|
||||
| `timeEstimate` | Int! | The time estimate on the issue |
|
||||
| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
|
||||
| `closedAt` | Time | |
|
||||
| `createdAt` | Time! | |
|
||||
| `updatedAt` | Time! | |
|
||||
| `taskCompletionStatus` | TaskCompletionStatus! | |
|
||||
| `epic` | Epic | The epic to which issue belongs |
|
||||
| `weight` | Int | |
|
||||
| `designs` | DesignCollection | |
|
||||
| `designCollection` | DesignCollection | |
|
||||
| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this issue |
|
||||
|
||||
### Group
|
||||
|
||||
| Name | Type | Description |
|
||||
|
@ -109,11 +318,13 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `visibility` | String | |
|
||||
| `lfsEnabled` | Boolean | |
|
||||
| `requestAccessEnabled` | Boolean | |
|
||||
| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available if the namespace has no parent |
|
||||
| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available for root namespaces |
|
||||
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
|
||||
| `webUrl` | String! | |
|
||||
| `avatarUrl` | String | |
|
||||
| `parent` | Group | |
|
||||
| `epicsEnabled` | Boolean | |
|
||||
| `epic` | Epic | |
|
||||
|
||||
### GroupPermissions
|
||||
|
||||
|
@ -144,10 +355,16 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `webPath` | String! | |
|
||||
| `webUrl` | String! | |
|
||||
| `relativePosition` | Int | |
|
||||
| `timeEstimate` | Int! | The time estimate on the issue |
|
||||
| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
|
||||
| `closedAt` | Time | |
|
||||
| `createdAt` | Time! | |
|
||||
| `updatedAt` | Time! | |
|
||||
| `taskCompletionStatus` | TaskCompletionStatus! | |
|
||||
| `epic` | Epic | The epic to which issue belongs |
|
||||
| `weight` | Int | |
|
||||
| `designs` | DesignCollection | |
|
||||
| `designCollection` | DesignCollection | |
|
||||
|
||||
### IssuePermissions
|
||||
|
||||
|
@ -158,6 +375,9 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `updateIssue` | Boolean! | Whether or not a user can perform `update_issue` on this resource |
|
||||
| `createNote` | Boolean! | Whether or not a user can perform `create_note` on this resource |
|
||||
| `reopenIssue` | Boolean! | Whether or not a user can perform `reopen_issue` on this resource |
|
||||
| `readDesign` | Boolean! | Whether or not a user can perform `read_design` on this resource |
|
||||
| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
|
||||
| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
|
||||
|
||||
### Label
|
||||
|
||||
|
@ -185,6 +405,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `updatedAt` | Time! | |
|
||||
| `sourceProject` | Project | |
|
||||
| `targetProject` | Project! | |
|
||||
| `diffRefs` | DiffRefs | |
|
||||
| `project` | Project! | |
|
||||
| `projectId` | Int! | |
|
||||
| `sourceProjectId` | Int | |
|
||||
|
@ -271,6 +492,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `visibility` | String | |
|
||||
| `lfsEnabled` | Boolean | |
|
||||
| `requestAccessEnabled` | Boolean | |
|
||||
| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available for root namespaces |
|
||||
|
||||
### Note
|
||||
|
||||
|
@ -381,7 +603,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `statistics` | ProjectStatistics | |
|
||||
| `repository` | Repository | |
|
||||
| `mergeRequest` | MergeRequest | |
|
||||
| `issue` | Issue | |
|
||||
| `issue` | ExtendedIssue | |
|
||||
|
||||
### ProjectPermissions
|
||||
|
||||
|
@ -424,6 +646,10 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `createPages` | Boolean! | Whether or not a user can perform `create_pages` on this resource |
|
||||
| `destroyPages` | Boolean! | Whether or not a user can perform `destroy_pages` on this resource |
|
||||
| `readPagesContent` | Boolean! | Whether or not a user can perform `read_pages_content` on this resource |
|
||||
| `adminOperations` | Boolean! | Whether or not a user can perform `admin_operations` on this resource |
|
||||
| `readDesign` | Boolean! | Whether or not a user can perform `read_design` on this resource |
|
||||
| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
|
||||
| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
|
||||
|
||||
### ProjectStatistics
|
||||
|
||||
|
@ -458,12 +684,12 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `storageSize` | Int! | The total storage in Bytes |
|
||||
| `repositorySize` | Int! | The Git repository size in Bytes |
|
||||
| `lfsObjectsSize` | Int! | The LFS objects size in Bytes |
|
||||
| `buildArtifactsSize` | Int! | The CI artifacts size in Bytes |
|
||||
| `packagesSize` | Int! | The packages size in Bytes |
|
||||
| `wikiSize` | Int! | The wiki size in Bytes |
|
||||
| `storageSize` | Int! | The total storage in bytes |
|
||||
| `repositorySize` | Int! | The git repository size in bytes |
|
||||
| `lfsObjectsSize` | Int! | The LFS objects size in bytes |
|
||||
| `buildArtifactsSize` | Int! | The CI artifacts size in bytes |
|
||||
| `packagesSize` | Int! | The packages size in bytes |
|
||||
| `wikiSize` | Int! | The wiki size in bytes |
|
||||
|
||||
### Submodule
|
||||
|
||||
|
@ -474,6 +700,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `type` | EntryType! | |
|
||||
| `path` | String! | |
|
||||
| `flatPath` | String! | |
|
||||
| `webUrl` | String | |
|
||||
| `treeUrl` | String | |
|
||||
|
||||
### TaskCompletionStatus
|
||||
|
||||
|
@ -495,7 +723,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `lastCommit` | Commit | |
|
||||
| `lastCommit` | Commit | Last commit for the tree |
|
||||
|
||||
### TreeEntry
|
||||
|
||||
|
@ -508,6 +736,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
|
|||
| `flatPath` | String! | |
|
||||
| `webUrl` | String | |
|
||||
|
||||
### UpdateNotePayload
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Reasons why the mutation failed. |
|
||||
| `note` | Note | The note after mutation |
|
||||
|
||||
### User
|
||||
|
||||
| Name | Type | Description |
|
||||
|
|
|
@ -26,6 +26,7 @@ GET /projects/:id/members
|
|||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `query` | string | no | A query string to search for members |
|
||||
| `user_ids` | array of integers | no | Filter the results on the given user IDs |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members
|
||||
|
@ -62,9 +63,8 @@ Example response:
|
|||
## List all members of a group or project including inherited members
|
||||
|
||||
Gets a list of group or project members viewable by the authenticated user, including inherited members through ancestor groups.
|
||||
When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project access_level (if exists)
|
||||
or the access_level for the user in the first group which he belongs to in the project groups ancestors chain.
|
||||
**Note:** We plan to [change](https://gitlab.com/gitlab-org/gitlab-foss/issues/62284) this behavior to return highest access_level instead.
|
||||
When a user is a member of the project/group and of one or more ancestor groups the user is returned only once with the project `access_level` (if exists)
|
||||
or the `access_level` for the user in the first group which he belongs to in the project groups ancestors chain.
|
||||
|
||||
```
|
||||
GET /groups/:id/members/all
|
||||
|
@ -75,6 +75,7 @@ GET /projects/:id/members/all
|
|||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `query` | string | no | A query string to search for members |
|
||||
| `user_ids` | array of integers | no | Filter the results on the given user IDs |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all
|
||||
|
@ -120,7 +121,7 @@ Example response:
|
|||
|
||||
## Get a member of a group or project
|
||||
|
||||
Gets a member of a group or project.
|
||||
Gets a member of a group or project. Returns only direct members and not inherited members through ancestor groups.
|
||||
|
||||
```
|
||||
GET /groups/:id/members/:user_id
|
||||
|
@ -152,6 +153,42 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Get a member of a group or project, including inherited members
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/17744) in GitLab 12.4.
|
||||
|
||||
Gets a member of a group or project, including members inherited through ancestor groups. See the corresponding [endpoint to list all inherited members](#list-all-members-of-a-group-or-project-including-inherited-members) for details.
|
||||
|
||||
```
|
||||
GET /groups/:id/members/all/:user_id
|
||||
GET /projects/:id/members/all/:user_id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `user_id` | integer | yes | The user ID of the member |
|
||||
|
||||
```bash
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/members/all/:user_id
|
||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/members/all/:user_id
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "raymond_smith",
|
||||
"name": "Raymond Smith",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
||||
"web_url": "http://192.168.1.8:3000/root",
|
||||
"access_level": 30,
|
||||
"expires_at": null
|
||||
}
|
||||
```
|
||||
|
||||
## Add a member to a group or project
|
||||
|
||||
Adds a member to a group or project.
|
||||
|
|
|
@ -289,6 +289,7 @@ are listed in the descriptions of the relevant settings.
|
|||
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
|
||||
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. |
|
||||
| `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory.
|
||||
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value. |
|
||||
| `recaptcha_enabled` | boolean | no | (**If enabled, requires:** `recaptcha_private_key` and `recaptcha_site_key`) Enable reCAPTCHA. |
|
||||
| `recaptcha_private_key` | string | required by: `recaptcha_enabled` | Private key for reCAPTCHA. |
|
||||
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |
|
||||
|
|
|
@ -14,7 +14,7 @@ tasks such as:
|
|||
To request access to Chatops on GitLab.com:
|
||||
|
||||
1. Log into <https://ops.gitlab.net/users/sign_in> **using the same username** as for GitLab.com (you may have to rename it).
|
||||
1. Ask [anyone in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
|
||||
1. Ask [an owner/maintainer in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members?search=&sort=access_level_desc) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
@ -492,19 +492,50 @@ For other punctuation rules, please refer to the
|
|||
|
||||
- Use inline link markdown markup `[Text](https://example.com)`.
|
||||
It's easier to read, review, and maintain. **Do not** use `[Text][identifier]`.
|
||||
- To link to internal documentation, use relative links, not full URLs. Use `../` to
|
||||
navigate to high-level directories, and always add the file name `file.md` at the
|
||||
end of the link with the `.md` extension, not `.html`.
|
||||
Example: instead of `[text](../../merge_requests/)`, use
|
||||
`[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or,
|
||||
for anchor links, `[text](../../ci/README.md#examples)`.
|
||||
Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
|
||||
section of GitLab.
|
||||
- To link from CE to EE-only documentation, use the EE-only doc full URL.
|
||||
|
||||
- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/).
|
||||
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
|
||||
write `Read more about [GitLab Issue Boards](LINK)`.
|
||||
|
||||
### Links to internal documentation
|
||||
|
||||
- To link to internal documentation, use relative links, not full URLs.
|
||||
Use `../` to navigate to high-level directories. Links should not refer to root.
|
||||
|
||||
Don't:
|
||||
|
||||
```md
|
||||
[Geo Troubleshooting](https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html)
|
||||
[Geo Troubleshooting](/ee/administration/geo/replication/troubleshooting.md)
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```md
|
||||
[Geo Troubleshooting](../../geo/replication/troubleshooting.md)
|
||||
```
|
||||
|
||||
- Always add the file name `file.md` at the end of the link with the `.md` extension, not `.html`.
|
||||
|
||||
Don't:
|
||||
|
||||
```md
|
||||
[merge requests](../../merge_requests/)
|
||||
[issues](../../issues/tags.html)
|
||||
[issue tags](../../issues/tags.html#stages)
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```md
|
||||
[merge requests](../../merge_requests/index.md)
|
||||
[issues](../../issues/tags.md)
|
||||
[issue tags](../../issues/tags.md#stages)
|
||||
```
|
||||
|
||||
- Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help)
|
||||
section of GitLab.
|
||||
|
||||
### Links requiring permissions
|
||||
|
||||
Don't link directly to:
|
||||
|
|
|
@ -20,9 +20,9 @@ should be added for EE. Licensed features can be stubbed using the
|
|||
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
|
||||
|
||||
You can force GitLab to act as CE by either deleting the `ee/` directory or by
|
||||
setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
|
||||
to something that evaluates as `false`. The same works for running tests
|
||||
(for example `IS_GITLAB_EE=0 yarn jest`).
|
||||
setting the [`FOSS_ONLY` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
|
||||
to something that evaluates as `true`. The same works for running tests
|
||||
(for example `FOSS_ONLY=1 yarn jest`).
|
||||
|
||||
[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab/issues/2500
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ These common definitions are:
|
|||
`docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services.
|
||||
- `.only-ee`: Only creates a job for the `gitlab` project.
|
||||
- `.only-ee-as-if-foss`: Same as `.only-ee` but simulate the FOSS project by
|
||||
setting the `IS_GITLAB_EE='0'` environment variable.
|
||||
setting the `FOSS_ONLY='1'` environment variable.
|
||||
|
||||
## Changes detection
|
||||
|
||||
|
@ -115,6 +115,7 @@ from a commit or MR by extending from the following CI definitions:
|
|||
- `.only-qa-changes`: Allows a job to only be created upon QA-related changes.
|
||||
- `.only-docs-changes`: Allows a job to only be created upon docs-related changes.
|
||||
- `.only-code-qa-changes`: Allows a job to only be created upon code-related or QA-related changes.
|
||||
- `.only-graphql-changes`: Allows a job to only be created upon graphql-related changes.
|
||||
|
||||
**See <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml>
|
||||
for the list of exact patterns.**
|
||||
|
@ -127,7 +128,7 @@ execute jobs out of order for the following jobs:
|
|||
```mermaid
|
||||
graph RL;
|
||||
A[setup-test-env];
|
||||
B["gitlab:assets:compile<br/>(master only)"];
|
||||
B["gitlab:assets:compile pull-push-cache<br/>(master only)"];
|
||||
C[gitlab:assets:compile pull-cache];
|
||||
D["cache gems<br/>(master and tags only)"];
|
||||
E[review-build-cng];
|
||||
|
@ -136,7 +137,7 @@ graph RL;
|
|||
G2["schedule:review-deploy<br/>(master only)"];
|
||||
H[karma];
|
||||
I[jest];
|
||||
J["compile-assets<br/>(master only)"];
|
||||
J["compile-assets pull-push-cache<br/>(master only)"];
|
||||
K[compile-assets pull-cache];
|
||||
L[webpack-dev-server];
|
||||
M[coverage];
|
||||
|
@ -145,39 +146,42 @@ graph RL;
|
|||
P["schedule:package-and-qa<br/>(master schedule only)"];
|
||||
Q[package-and-qa];
|
||||
R[package-and-qa-manual];
|
||||
S["RSpec<br/>(e.g. rspec unit pg9)"]
|
||||
T[retrieve-tests-metadata];
|
||||
|
||||
subgraph "`prepare` stage"
|
||||
A
|
||||
F
|
||||
J
|
||||
K
|
||||
J
|
||||
T
|
||||
end
|
||||
|
||||
subgraph "`test` stage"
|
||||
B --> |needs| A;
|
||||
C --> |needs| A;
|
||||
D --> |needs| A;
|
||||
H -.-> |depends on| A;
|
||||
H -.-> |depends on| J;
|
||||
H -.-> |depends on| K;
|
||||
I -.-> |depends on| A;
|
||||
I -.-> |depends on| J;
|
||||
I -.-> |depends on| K;
|
||||
L -.-> |depends on| A;
|
||||
L -.-> |depends on| J;
|
||||
L -.-> |depends on| K;
|
||||
H -.-> |needs and depends on| A;
|
||||
H -.-> |needs and depends on| K;
|
||||
I -.-> |needs and depends on| A;
|
||||
I -.-> |needs and depends on| K;
|
||||
L -.-> |needs and depends on| A;
|
||||
L -.-> |needs and depends on| K;
|
||||
O -.-> |needs and depends on| A;
|
||||
O -.-> |needs and depends on| K;
|
||||
S -.-> |needs and depends on| A;
|
||||
S -.-> |needs and depends on| K;
|
||||
S -.-> |needs and depends on| T;
|
||||
downtime_check --> |needs and depends on| A;
|
||||
db:* --> |needs| A;
|
||||
gitlab:setup --> |needs| A;
|
||||
O -.-> |depends on| A;
|
||||
O -.-> |depends on| B;
|
||||
O -.-> |depends on| C;
|
||||
downtime_check --> |needs and depends on| A;
|
||||
graphql-docs-verify --> |needs| A;
|
||||
end
|
||||
|
||||
subgraph "`review-prepare` stage"
|
||||
E --> |needs| C;
|
||||
X["schedule:review-build-cng<br/>(master schedule only)"] --> |needs| B;
|
||||
X["schedule:review-build-cng<br/>(master schedule only)"] --> |needs| C;
|
||||
end
|
||||
|
||||
subgraph "`review` stage"
|
||||
|
@ -190,7 +194,7 @@ subgraph "`qa` stage"
|
|||
Q --> |needs| F;
|
||||
R --> |needs| C;
|
||||
R --> |needs| F;
|
||||
P --> |needs| B;
|
||||
P --> |needs| C;
|
||||
P --> |needs| F;
|
||||
review-qa-smoke -.-> |needs and depends on| G;
|
||||
review-qa-all -.-> |needs and depends on| G;
|
||||
|
@ -209,7 +213,7 @@ subgraph "`post-test` stage"
|
|||
end
|
||||
|
||||
subgraph "`pages` stage"
|
||||
N -.-> |depends on| B;
|
||||
N -.-> |depends on| C;
|
||||
N -.-> |depends on| H;
|
||||
N -.-> |depends on| M;
|
||||
end
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
Binary file not shown.
Before Width: | Height: | Size: 173 KiB |
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
Before Width: | Height: | Size: 95 KiB |
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
|
@ -10,13 +10,13 @@ Epics let you manage your portfolio of projects more efficiently and with less
|
|||
effort by tracking groups of issues that share a theme, across projects and
|
||||
milestones.
|
||||
|
||||
![epics list view](img/epics_list_view.png)
|
||||
![epics list view](img/epics_list_view_v12.3.png)
|
||||
|
||||
## Use cases
|
||||
|
||||
- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
|
||||
- Track when the work for the group of issues is targeted to begin, and when it is targeted to end.
|
||||
- Discuss and collaborate on feature ideas and scope at a high-level.
|
||||
- Discuss and collaborate on feature ideas and scope at a high level.
|
||||
|
||||
## Creating an epic
|
||||
|
||||
|
@ -24,78 +24,114 @@ A paginated list of epics is available in each group from where you can create
|
|||
a new epic. The list of epics includes also epics from all subgroups of the
|
||||
selected group. From your group page:
|
||||
|
||||
1. Go to **Epics**
|
||||
1. Click the **New epic** button at the top right
|
||||
1. Enter a descriptive title and hit **Create epic**
|
||||
1. Go to **Epics**.
|
||||
1. Click **New epic**.
|
||||
1. Enter a descriptive title and click **Create epic**.
|
||||
|
||||
Once created, you will be taken to the view for that newly-created epic where
|
||||
you can change its title, description, start date, and due date.
|
||||
You will be taken to the new epic where can edit the following details:
|
||||
|
||||
![epic view](img/epic_view.png)
|
||||
- Title
|
||||
- Description
|
||||
- Start date
|
||||
- Due date
|
||||
- Labels
|
||||
|
||||
An epic's page contains the following tabs:
|
||||
|
||||
- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
|
||||
- Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
|
||||
- **Roadmap**: a roadmap view of child epics which have start and due dates.
|
||||
|
||||
![epic view](img/epic_view_v12.3.png)
|
||||
|
||||
## Adding an issue to an epic
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
1. Click the plus icon (<kbd>+</kbd>) under the epic description.
|
||||
1. Paste the link of the issue (you can hit <kbd>Spacebar</kbd> to add more than
|
||||
one issues at a time).
|
||||
An epic contains a list of issues and an issue can be associated with at most
|
||||
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 **Add an issue**.
|
||||
1. Paste the link of the issue.
|
||||
- Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
|
||||
1. Click **Add**.
|
||||
|
||||
Any issue belonging to a project in the epic's group or any of the epic's
|
||||
subgroups are eligible to be added. To remove an issue from an epic, click
|
||||
on the <kbd>x</kbd> button in the epic's issue list.
|
||||
To remove an issue from an epic:
|
||||
|
||||
NOTE: **Note:**
|
||||
When you add an issue or an epic to an epic that's already associated with another epic,
|
||||
the issue or the epic is automatically removed from the previous epic.
|
||||
1. Click on the <kbd>x</kbd> button in the epic's issue list.
|
||||
1. Click **Remove** in the **Remove issue** warning message.
|
||||
|
||||
## Multi-level child epics
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7.
|
||||
|
||||
Much like adding issues to an epic, an epic can have multiple child epics with
|
||||
the maximum depth being 5. To add a child epic:
|
||||
Any epic that belongs to a group, or subgroup of the parent epic's group, is
|
||||
eligible to be added. New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
|
||||
|
||||
1. Click the plus icon (<kbd>+</kbd>) under the epic description.
|
||||
When you add a child epic that is already associated with another epic,
|
||||
that epic is automatically removed from the previous epic.
|
||||
|
||||
An epic can have multiple child epics with
|
||||
the maximum depth being 5.
|
||||
|
||||
To add a child epic:
|
||||
|
||||
1. Click **Add an epic**.
|
||||
1. Paste the link of the epic.
|
||||
- Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
|
||||
1. Click **Add**.
|
||||
|
||||
Any epic that belongs to a group or subgroup of the parent epic's group is
|
||||
eligible to be added. To remove a child epic from a parent epic,
|
||||
click on the <kbd>x</kbd> button in the parent epic's epic list.
|
||||
To remove a child epic from a parent epic:
|
||||
|
||||
1. Click on the <kbd>x</kbd> button in the parent epic's list of epics.
|
||||
1. Click **Remove** in the **Remove epic** warning message.
|
||||
|
||||
## Start date and due date
|
||||
|
||||
For each of the dates in the sidebar of an epic, you can choose to either:
|
||||
To set a **Start date** and **Due date** for an epic, you can choose either of the following:
|
||||
|
||||
- Enter a fixed value.
|
||||
- Inherit a dynamic value called "From milestones".
|
||||
- **Fixed**: Enter a fixed value.
|
||||
- **From milestones:** Inherit a dynamic value from the issues added to the epic.
|
||||
|
||||
If you select "From milestones" for the start date, GitLab will automatically set the
|
||||
If you select **From milestones** for the start date, GitLab will automatically set the
|
||||
date to be earliest start date across all milestones that are currently assigned
|
||||
to the issues that are attached to the epic. Similarly, if you select "From milestones"
|
||||
to the issues that are added to the epic. Similarly, if you select "From milestones"
|
||||
for the due date, GitLab will set it to be the latest due date across all
|
||||
milestones that are currently assigned to those issues.
|
||||
|
||||
These are dynamic dates in that if milestones are re-assigned to the issues, if the
|
||||
milestone dates change, or if issues are added or removed from the epic, then
|
||||
the re-calculation will happen immediately to set a new dynamic date.
|
||||
These are dynamic dates which are recalculated immediately if any of the following occur:
|
||||
|
||||
## Roadmap in epics
|
||||
- Milestones are re-assigned to the issues.
|
||||
- Milestone dates change.
|
||||
- Issues are added or removed from the epic.
|
||||
|
||||
## Roadmap
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
|
||||
|
||||
If your epic contains one or more [child epics](#multi-level-child-epics) which
|
||||
have a [start or due date](#start-date-and-due-date), then you can see a
|
||||
[roadmap](../roadmap/index.md) view of the child epics under the parent epic itself.
|
||||
have a [start or due date](#start-date-and-due-date), a
|
||||
[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic.
|
||||
|
||||
![Child epics roadmap](img/child_epics_roadmap.png)
|
||||
![Child epics roadmap](img/epic_view_roadmap_v12.3.png)
|
||||
|
||||
## Reordering issues and child epics
|
||||
|
||||
Drag and drop to reorder issues and child epics. New issues and child epics added to an epic appear at the top of the list.
|
||||
New issues and child epics are added to the top of their respective lists in the **Epics and Issues** tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot be intermingled.
|
||||
|
||||
To reorder issues assigned to an epic:
|
||||
|
||||
1. Go to the **Epics and Issues** tab.
|
||||
1. Drag and drop issues into the desired order.
|
||||
|
||||
To reorder child epics assigned to an epic:
|
||||
|
||||
1. Go to the **Epics and Issues** tab.
|
||||
1. Drag and drop epics into the desired order.
|
||||
|
||||
## Updating epics
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ Epics in the view can be sorted by:
|
|||
Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
|
||||
including the [epics list view](../epics/index.md).
|
||||
|
||||
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
|
||||
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap).
|
||||
|
||||
## Timeline duration
|
||||
|
||||
|
|
|
@ -170,7 +170,7 @@ the `distributionManagement` section:
|
|||
<repositories>
|
||||
<repository>
|
||||
<id>gitlab-maven</id>
|
||||
<url>https://gitlab.com/api/v4/groups/my-group/-/packages/maven</url>
|
||||
<url>https://gitlab.com/api/v4/groups/GROUP_ID/-/packages/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<distributionManagement>
|
||||
|
|
|
@ -56,6 +56,16 @@ Click on the service links to see further configuration instructions and details
|
|||
| [Redmine](redmine.md) | Redmine issue tracker |
|
||||
| [YouTrack](youtrack.md) | YouTrack issue tracker |
|
||||
|
||||
## Push hooks limit
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31009) in GitLab 12.4.
|
||||
|
||||
If a single push includes changes to more than three branches or tags, services
|
||||
supported by `push_hooks` and `tag_push_hooks` events won't be executed.
|
||||
|
||||
The number of branches or tags supported can be changed via
|
||||
[`push_event_hooks_limit` application setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls).
|
||||
|
||||
## Services templates
|
||||
|
||||
Services templates is a way to set some predefined values in the Service of
|
||||
|
|
|
@ -107,6 +107,9 @@ detailed commit data is expensive. Note that despite only 20 commits being
|
|||
present in the `commits` attribute, the `total_commits_count` attribute will
|
||||
contain the actual total.
|
||||
|
||||
Also, if a single push includes changes for more than three (by default, depending on
|
||||
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)) branches, this hook won't be executed.
|
||||
|
||||
**Request header**:
|
||||
|
||||
```
|
||||
|
@ -190,6 +193,10 @@ X-Gitlab-Event: Push Hook
|
|||
|
||||
Triggered when you create (or delete) tags to the repository.
|
||||
|
||||
NOTE: **Note:**
|
||||
If a single push includes changes for more than three (by default, depending on
|
||||
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)) tags, this hook won't be executed.
|
||||
|
||||
**Request header**:
|
||||
|
||||
```
|
||||
|
|
|
@ -42,6 +42,88 @@ module API
|
|||
|
||||
present deployment, with: Entities::Deployment
|
||||
end
|
||||
|
||||
desc 'Creates a new deployment' do
|
||||
detail 'This feature was introduced in GitLab 12.4'
|
||||
success Entities::Deployment
|
||||
end
|
||||
params do
|
||||
requires :environment,
|
||||
type: String,
|
||||
desc: 'The name of the environment to deploy to'
|
||||
|
||||
requires :sha,
|
||||
type: String,
|
||||
desc: 'The SHA of the commit that was deployed'
|
||||
|
||||
requires :ref,
|
||||
type: String,
|
||||
desc: 'The name of the branch or tag that was deployed'
|
||||
|
||||
requires :tag,
|
||||
type: Boolean,
|
||||
desc: 'A boolean indicating if the deployment ran for a tag'
|
||||
|
||||
requires :status,
|
||||
type: String,
|
||||
desc: 'The status of the deployment',
|
||||
values: %w[running success failed canceled]
|
||||
end
|
||||
post ':id/deployments' do
|
||||
authorize!(:create_deployment, user_project)
|
||||
authorize!(:create_environment, user_project)
|
||||
|
||||
environment = user_project
|
||||
.environments
|
||||
.find_or_create_by_name(params[:environment])
|
||||
|
||||
unless environment.persisted?
|
||||
render_validation_error!(deployment)
|
||||
end
|
||||
|
||||
authorize!(:create_deployment, environment)
|
||||
|
||||
service = ::Deployments::CreateService
|
||||
.new(environment, current_user, declared_params)
|
||||
|
||||
deployment = service.execute
|
||||
|
||||
if deployment.persisted?
|
||||
present(deployment, with: Entities::Deployment, current_user: current_user)
|
||||
else
|
||||
render_validation_error!(deployment)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Updates an existing deployment' do
|
||||
detail 'This feature was introduced in GitLab 12.4'
|
||||
success Entities::Deployment
|
||||
end
|
||||
params do
|
||||
requires :status,
|
||||
type: String,
|
||||
desc: 'The new status of the deployment',
|
||||
values: %w[running success failed canceled]
|
||||
end
|
||||
put ':id/deployments/:deployment_id' do
|
||||
authorize!(:read_deployment, user_project)
|
||||
|
||||
deployment = user_project.deployments.find(params[:deployment_id])
|
||||
|
||||
authorize!(:update_deployment, deployment)
|
||||
|
||||
if deployment.deployable
|
||||
forbidden!('Deployments created using GitLab CI can not be updated using the API')
|
||||
end
|
||||
|
||||
service = ::Deployments::UpdateService.new(deployment, declared_params)
|
||||
|
||||
if service.execute
|
||||
present(deployment, with: Entities::Deployment, current_user: current_user)
|
||||
else
|
||||
render_validation_error!(deployment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,7 @@ module API
|
|||
end
|
||||
params do
|
||||
optional :query, type: String, desc: 'A query string to search for members'
|
||||
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
|
||||
use :pagination
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
@ -26,6 +27,7 @@ module API
|
|||
|
||||
members = source.members.where.not(user_id: nil).includes(:user)
|
||||
members = members.joins(:user).merge(User.search(params[:query])) if params[:query].present?
|
||||
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
|
||||
members = paginate(members)
|
||||
|
||||
present members, with: Entities::Member
|
||||
|
@ -37,6 +39,7 @@ module API
|
|||
end
|
||||
params do
|
||||
optional :query, type: String, desc: 'A query string to search for members'
|
||||
optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
|
||||
use :pagination
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
@ -45,6 +48,7 @@ module API
|
|||
|
||||
members = find_all_members(source_type, source)
|
||||
members = members.includes(:user).references(:user).merge(User.search(params[:query])) if params[:query].present?
|
||||
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
|
||||
members = paginate(members)
|
||||
|
||||
present members, with: Entities::Member
|
||||
|
@ -68,6 +72,23 @@ module API
|
|||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
desc 'Gets a member of a group or project, including those who gained membership through ancestor group' do
|
||||
success Entities::Member
|
||||
end
|
||||
params do
|
||||
requires :user_id, type: Integer, desc: 'The user ID of the member'
|
||||
end
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
get ":id/members/all/:user_id" do
|
||||
source = find_source(source_type, params[:id])
|
||||
|
||||
members = find_all_members(source_type, source)
|
||||
member = members.find_by!(user_id: params[:user_id])
|
||||
|
||||
present member, with: Entities::Member
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
desc 'Adds a member to a group or project.' do
|
||||
success Entities::Member
|
||||
end
|
||||
|
|
|
@ -101,6 +101,7 @@ module API
|
|||
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
|
||||
optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
|
||||
optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics'
|
||||
optional :push_event_hooks_limit, type: Integer, desc: "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
|
||||
optional :recaptcha_enabled, type: Boolean, desc: 'Helps prevent bots from creating accounts'
|
||||
given recaptcha_enabled: ->(val) { val } do
|
||||
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
|
||||
|
|
|
@ -69,14 +69,14 @@ module Gitlab
|
|||
# means that checking the presence of the License class could result in
|
||||
# this method returning `false`, even for an EE installation.
|
||||
#
|
||||
# The `IS_GITLAB_EE` is always `string` or `nil`
|
||||
# The `FOSS_ONLY` is always `string` or `nil`
|
||||
# Thus the nil or empty string will result
|
||||
# in using default value: true
|
||||
# in using default value: false
|
||||
#
|
||||
# The behavior needs to be synchronised with
|
||||
# config/helpers/is_ee_env.js
|
||||
root.join('ee/app/models/license.rb').exist? &&
|
||||
(ENV['IS_GITLAB_EE'].to_s.empty? || Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE']))
|
||||
!%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
|
||||
end
|
||||
|
||||
def self.ee
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Convert terminal stream to JSON
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Ansi2json
|
||||
def self.convert(ansi, state = nil)
|
||||
Converter.new.convert(ansi, state)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,131 @@
|
|||
# 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 << '<'
|
||||
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
|
|
@ -0,0 +1,93 @@
|
|||
# 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
|
|
@ -0,0 +1,200 @@
|
|||
# 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
|
|
@ -0,0 +1,98 @@
|
|||
# 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
|
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
module Ansi2json
|
||||
class Style
|
||||
attr_reader :fg, :bg, :mask
|
||||
|
||||
def initialize(fg: nil, bg: nil, mask: 0)
|
||||
@fg = fg
|
||||
@bg = bg
|
||||
@mask = mask
|
||||
|
||||
update_formats
|
||||
end
|
||||
|
||||
def update(ansi_commands)
|
||||
command = ansi_commands.shift
|
||||
return unless command
|
||||
|
||||
if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
|
||||
apply_changes(changes)
|
||||
end
|
||||
|
||||
update(ansi_commands)
|
||||
end
|
||||
|
||||
def set?
|
||||
@fg || @bg || @formats.any?
|
||||
end
|
||||
|
||||
def reset!
|
||||
@fg = nil
|
||||
@bg = nil
|
||||
@mask = 0
|
||||
@formats = []
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
self.to_h == other.to_h
|
||||
end
|
||||
|
||||
def to_s
|
||||
[@fg, @bg, @formats].flatten.compact.join(' ')
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ fg: @fg, bg: @bg, mask: @mask }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_changes(changes)
|
||||
case
|
||||
when changes[:reset]
|
||||
reset!
|
||||
when changes[:fg]
|
||||
@fg = changes[:fg]
|
||||
when changes[:bg]
|
||||
@bg = changes[:bg]
|
||||
when changes[:enable]
|
||||
@mask |= changes[:enable]
|
||||
when changes[:disable]
|
||||
@mask &= ~changes[:disable]
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
update_formats
|
||||
end
|
||||
|
||||
def update_formats
|
||||
# Most terminals show bold colored text in the light color variant
|
||||
# Let's mimic that here
|
||||
if @fg.present? && Gitlab::Ci::Ansi2json::Parser.bold?(@mask)
|
||||
@fg = @fg.sub(/fg-([a-z]{2,}+)/, 'fg-l-\1')
|
||||
end
|
||||
|
||||
@formats = Gitlab::Ci::Ansi2json::Parser.matching_formats(@mask)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,7 +12,7 @@ module Gitlab
|
|||
|
||||
def value
|
||||
strong_memoize(:value) do
|
||||
query = @project.deployments.where("created_at >= ?", @from)
|
||||
query = @project.deployments.success.where("created_at >= ?", @from)
|
||||
query = query.where("created_at <= ?", @to) if @to
|
||||
query.count
|
||||
end
|
||||
|
|
|
@ -6,13 +6,13 @@ module Gitlab
|
|||
include Enumerable
|
||||
|
||||
# collection - An array of Gitlab::Diff::Position
|
||||
def initialize(collection, diff_head_sha)
|
||||
def initialize(collection, diff_head_sha = nil)
|
||||
@collection = collection
|
||||
@diff_head_sha = diff_head_sha
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@collection.each(&block)
|
||||
filtered_positions.each(&block)
|
||||
end
|
||||
|
||||
def concat(positions)
|
||||
|
@ -23,9 +23,21 @@ module Gitlab
|
|||
# positions (https://gitlab.com/gitlab-org/gitlab/issues/33271).
|
||||
def unfoldable
|
||||
select do |position|
|
||||
position.unfoldable? && position.head_sha == @diff_head_sha
|
||||
position.unfoldable? && valid_head_sha?(position)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filtered_positions
|
||||
@collection.select { |item| item.is_a?(Position) }
|
||||
end
|
||||
|
||||
def valid_head_sha?(position)
|
||||
return true unless @diff_head_sha
|
||||
|
||||
position.head_sha == @diff_head_sha
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,15 +23,12 @@ module Gitlab
|
|||
@parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse
|
||||
end
|
||||
|
||||
def render
|
||||
contents = @layout.render(self)
|
||||
|
||||
write_file(contents)
|
||||
def contents
|
||||
# Render and remove an extra trailing new line
|
||||
@contents ||= @layout.render(self).sub!(/\n(?=\Z)/, '')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_file(contents)
|
||||
def write
|
||||
filename = File.join(@output_dir, 'index.md')
|
||||
|
||||
FileUtils.mkdir_p(@output_dir)
|
||||
|
|
|
@ -20,6 +20,3 @@
|
|||
- type[:fields].each do |field|
|
||||
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
|
||||
\
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module HealthChecks
|
||||
CHECKS = [
|
||||
Gitlab::HealthChecks::DbCheck,
|
||||
Gitlab::HealthChecks::Redis::RedisCheck,
|
||||
Gitlab::HealthChecks::Redis::CacheCheck,
|
||||
Gitlab::HealthChecks::Redis::QueuesCheck,
|
||||
Gitlab::HealthChecks::Redis::SharedStateCheck,
|
||||
Gitlab::HealthChecks::GitalyCheck
|
||||
].freeze
|
||||
end
|
||||
end
|
|
@ -3,14 +3,13 @@
|
|||
module Gitlab
|
||||
module HealthChecks
|
||||
module Probes
|
||||
class Readiness
|
||||
class Collection
|
||||
attr_reader :checks
|
||||
|
||||
# This accepts an array of objects implementing `:readiness`
|
||||
# that returns `::Gitlab::HealthChecks::Result`
|
||||
def initialize(*additional_checks)
|
||||
@checks = ::Gitlab::HealthChecks::CHECKS
|
||||
@checks += additional_checks
|
||||
def initialize(*checks)
|
||||
@checks = checks
|
||||
end
|
||||
|
||||
def execute
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module HealthChecks
|
||||
module Probes
|
||||
class Liveness
|
||||
def execute
|
||||
Probes::Status.new(200, status: 'ok')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ module Gitlab
|
|||
class BaseExporter < Daemon
|
||||
attr_reader :server
|
||||
|
||||
attr_accessor :additional_checks
|
||||
attr_accessor :readiness_checks
|
||||
|
||||
def enabled?
|
||||
settings.enabled
|
||||
|
@ -73,11 +73,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def readiness_probe
|
||||
::Gitlab::HealthChecks::Probes::Readiness.new(*additional_checks)
|
||||
::Gitlab::HealthChecks::Probes::Collection.new(*readiness_checks)
|
||||
end
|
||||
|
||||
def liveness_probe
|
||||
::Gitlab::HealthChecks::Probes::Liveness.new
|
||||
::Gitlab::HealthChecks::Probes::Collection.new
|
||||
end
|
||||
|
||||
def render_probe(probe, req, res)
|
||||
|
|
|
@ -20,7 +20,7 @@ module Gitlab
|
|||
def initialize
|
||||
super
|
||||
|
||||
self.additional_checks = [
|
||||
self.readiness_checks = [
|
||||
WebExporter::ExporterCheck.new(self),
|
||||
Gitlab::HealthChecks::PumaCheck,
|
||||
Gitlab::HealthChecks::UnicornCheck
|
||||
|
|
|
@ -11,10 +11,28 @@ namespace :gitlab do
|
|||
task compile_docs: :environment do
|
||||
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
|
||||
|
||||
renderer.render
|
||||
renderer.write
|
||||
|
||||
puts "Documentation compiled."
|
||||
end
|
||||
|
||||
desc 'GitLab | Check if GraphQL docs are up to date'
|
||||
task check_docs: :environment do
|
||||
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
|
||||
|
||||
doc = File.read(Rails.root.join(OUTPUT_DIR, 'index.md'))
|
||||
|
||||
if doc == renderer.contents
|
||||
puts "GraphQL documentation is up to date"
|
||||
else
|
||||
puts '#' * 10
|
||||
puts '#'
|
||||
puts '# GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.'
|
||||
puts '#'
|
||||
puts '#' * 10
|
||||
abort
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue