summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-16 18:08:01 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-16 18:08:01 +0000
commit8e45d25f7dde6508839ffee719c0ddc2cf6b12d3 (patch)
tree9839e7fe63b36904d40995ebf519124c9a8f7681
parent00c78fb814d7ce00989ac04edd6cdaa3239da284 (diff)
downloadgitlab-ce-8e45d25f7dde6508839ffee719c0ddc2cf6b12d3.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitattributes1
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml15
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml36
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml8
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml25
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml4
-rw-r--r--app/assets/javascripts/api.js17
-rw-r--r--app/assets/javascripts/pages/projects/releases/edit/index.js7
-rw-r--r--app/assets/javascripts/releases/detail/components/app.vue156
-rw-r--r--app/assets/javascripts/releases/detail/index.js19
-rw-r--r--app/assets/javascripts/releases/detail/store/actions.js62
-rw-r--r--app/assets/javascripts/releases/detail/store/index.js14
-rw-r--r--app/assets/javascripts/releases/detail/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/releases/detail/store/mutations.js42
-rw-r--r--app/assets/javascripts/releases/detail/store/state.js15
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/pages/tags.scss3
-rw-r--r--app/controllers/health_controller.rb21
-rw-r--r--app/controllers/projects/deployments_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb8
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/environment_helper.rb33
-rw-r--r--app/helpers/releases_helper.rb10
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/deployment.rb8
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/project.rb2
-rw-r--r--app/policies/deployment_policy.rb14
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/services/deployments/after_create_service.rb60
-rw-r--r--app/services/deployments/create_service.rb39
-rw-r--r--app/services/deployments/update_service.rb16
-rw-r--r--app/services/git/base_hooks_service.rb2
-rw-r--r--app/services/git/process_ref_changes_service.rb11
-rw-r--r--app/services/update_deployment_service.rb57
-rw-r--r--app/views/admin/application_settings/_performance.html.haml5
-rw-r--r--app/views/projects/blob/show.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml38
-rw-r--r--app/views/projects/deployments/_rollback.haml2
-rw-r--r--app/views/projects/environments/show.html.haml9
-rw-r--r--app/views/projects/issues/import_csv/_button.html.haml2
-rw-r--r--app/views/projects/releases/edit.html.haml3
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml6
-rw-r--r--app/workers/deployments/success_worker.rb2
-rw-r--r--changelogs/unreleased/27715-fix-unrenderable-notes.yml5
-rw-r--r--changelogs/unreleased/31009-limit-project-hooks-services.yml5
-rw-r--r--changelogs/unreleased/32380-update-issue-list-icons.yml5
-rw-r--r--changelogs/unreleased/add-ansi2json-log-parser.yml5
-rw-r--r--changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml5
-rw-r--r--changelogs/unreleased/deployments-api.yml5
-rw-r--r--changelogs/unreleased/nfriend-add-edit-release-page.yml5
-rw-r--r--changelogs/unreleased/nfriend-fix-lin.yml5
-rw-r--r--config/helpers/is_ee_env.js8
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb17
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/high_availability/README.md33
-rw-r--r--doc/api/deployments.md97
-rw-r--r--doc/api/graphql/reference/index.md260
-rw-r--r--doc/api/members.md45
-rw-r--r--doc/api/settings.md1
-rw-r--r--doc/development/chatops_on_gitlabcom.md2
-rw-r--r--doc/development/documentation/styleguide.md49
-rw-r--r--doc/development/ee_features.md6
-rw-r--r--doc/development/pipelines.md42
-rw-r--r--doc/user/group/epics/img/child_epics_roadmap.pngbin30149 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/epic_view.pngbin176740 -> 0 bytes
-rwxr-xr-xdoc/user/group/epics/img/epic_view_roadmap_v12.3.pngbin0 -> 50491 bytes
-rwxr-xr-xdoc/user/group/epics/img/epic_view_v12.3.pngbin0 -> 61402 bytes
-rw-r--r--doc/user/group/epics/img/epics_list_view.pngbin96826 -> 0 bytes
-rwxr-xr-xdoc/user/group/epics/img/epics_list_view_v12.3.pngbin0 -> 39450 bytes
-rw-r--r--doc/user/group/epics/index.md110
-rw-r--r--doc/user/group/roadmap/index.md2
-rw-r--r--doc/user/packages/maven_repository/index.md2
-rw-r--r--doc/user/project/integrations/project_services.md10
-rw-r--r--doc/user/project/integrations/webhooks.md7
-rw-r--r--lib/api/deployments.rb82
-rw-r--r--lib/api/members.rb21
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/gitlab.rb6
-rw-r--r--lib/gitlab/ci/ansi2json.rb12
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb131
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb93
-rw-r--r--lib/gitlab/ci/ansi2json/parser.rb200
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb98
-rw-r--r--lib/gitlab/ci/ansi2json/style.rb84
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb2
-rw-r--r--lib/gitlab/diff/position_collection.rb18
-rw-r--r--lib/gitlab/graphql/docs/renderer.rb11
-rw-r--r--lib/gitlab/graphql/docs/templates/default.md.haml3
-rw-r--r--lib/gitlab/health_checks/checks.rb14
-rw-r--r--lib/gitlab/health_checks/probes/collection.rb (renamed from lib/gitlab/health_checks/probes/readiness.rb)7
-rw-r--r--lib/gitlab/health_checks/probes/liveness.rb13
-rw-r--r--lib/gitlab/metrics/exporter/base_exporter.rb6
-rw-r--r--lib/gitlab/metrics/exporter/web_exporter.rb2
-rw-r--r--lib/tasks/gitlab/graphql.rake20
-rw-r--r--locale/gitlab.pot51
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb17
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb4
-rw-r--r--spec/features/projects/environments/environment_spec.rb10
-rw-r--r--spec/fixtures/api/schemas/deployment.json2
-rw-r--r--spec/frontend/releases/detail/components/app_spec.js70
-rw-r--r--spec/frontend/releases/detail/store/actions_spec.js217
-rw-r--r--spec/frontend/releases/detail/store/mutations_spec.js119
-rw-r--r--spec/helpers/environment_helper_spec.rb25
-rw-r--r--spec/javascripts/test_bundle.js6
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/ansi2json/parser_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/ansi2json/style_spec.rb166
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb544
-rw-r--r--spec/lib/gitlab/diff/position_collection_spec.rb13
-rw-r--r--spec/lib/gitlab/health_checks/probes/collection_spec.rb62
-rw-r--r--spec/lib/gitlab/health_checks/probes/liveness_spec.rb17
-rw-r--r--spec/lib/gitlab/health_checks/probes/readiness_spec.rb39
-rw-r--r--spec/lib/gitlab_spec.rb78
-rw-r--r--spec/models/application_setting_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb13
-rw-r--r--spec/models/environment_spec.rb15
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/requests/api/deployments_spec.rb162
-rw-r--r--spec/requests/api/members_spec.rb29
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/services/deployments/after_create_service_spec.rb (renamed from spec/services/update_deployment_service_spec.rb)2
-rw-r--r--spec/services/deployments/create_service_spec.rb92
-rw-r--r--spec/services/deployments/update_service_spec.rb15
-rw-r--r--spec/services/git/base_hooks_service_spec.rb29
-rw-r--r--spec/services/git/process_ref_changes_service_spec.rb54
-rw-r--r--spec/support/features/rss_shared_examples.rb8
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb4
-rw-r--r--spec/workers/deployments/success_worker_spec.rb12
-rw-r--r--spec/workers/post_receive_spec.rb2
135 files changed, 4051 insertions, 407 deletions
diff --git a/.gitattributes b/.gitattributes
index 0b87a97df9c..55c422f0f8c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,2 @@
VERSION merge=ours
Dangerfile gitlab-language=ruby
-db/schema.rb merge=merge_db_schema
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 008151f889f..cc0999b50c6 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -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
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index a3a2ab0691f..2f457bc0ee2 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -53,7 +53,7 @@
- gitlab-org
- docker
-gitlab:assets:compile:
+gitlab:assets:compile pull-push-cache:
extends: .gitlab:assets:compile-metadata
only:
refs:
@@ -63,9 +63,6 @@ gitlab:assets:compile:
gitlab:assets:compile pull-cache:
extends: .gitlab:assets:compile-metadata
- except:
- refs:
- - master
cache:
policy: pull
@@ -89,14 +86,14 @@ gitlab:assets:compile pull-cache:
# we override the max_old_space_size to prevent OOM errors
NODE_OPTIONS: --max_old_space_size=3584
cache:
- key: "assets-compile:test:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v6"
+ key: "assets-compile:v7"
artifacts:
expire_in: 7d
paths:
- node_modules
- public/assets
-compile-assets:
+compile-assets pull-push-cache:
extends: .compile-assets-metadata
only:
refs:
@@ -104,13 +101,25 @@ compile-assets:
cache:
policy: pull-push
-compile-assets pull-cache:
- extends: .compile-assets-metadata
- except:
+compile-assets pull-push-cache foss:
+ extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
+ only:
refs:
- master
cache:
+ policy: pull-push
+ key: "assets-compile:v7:foss"
+
+compile-assets pull-cache:
+ extends: .compile-assets-metadata
+ cache:
+ policy: pull
+
+compile-assets pull-cache foss:
+ extends: [".compile-assets-metadata", ".only-ee-as-if-foss"]
+ cache:
policy: pull
+ key: "assets-compile:v7:foss"
.only-code-frontend-job-base:
extends:
@@ -121,7 +130,9 @@ compile-assets pull-cache:
- .default-before_script
- .only-code-changes
- .use-pg9
- dependencies: ["compile-assets", "compile-assets pull-cache", "setup-test-env"]
+ stage: test
+ needs: ["setup-test-env", "compile-assets pull-cache"]
+ dependencies: ["setup-test-env", "compile-assets pull-cache"]
.karma-base:
extends: .only-code-frontend-job-base
@@ -195,6 +206,7 @@ jest-foss:
- .default-cache
- .default-only
- .only-code-changes
+ stage: test
dependencies: []
cache:
key: "$CI_JOB_NAME"
@@ -227,7 +239,9 @@ webpack-dev-server:
- .default-cache
- .default-only
- .only-code-changes
- dependencies: ["setup-test-env", "compile-assets", "compile-assets pull-cache"]
+ stage: test
+ needs: ["setup-test-env", "compile-assets pull-cache"]
+ dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables:
WEBPACK_MEMORY_TEST: "true"
script:
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index af7c7a0d152..fc9b00b5d3c 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -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'
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index a59b84fe1cf..a30772d5664 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -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/
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index a73edd3f65f..1194948a76f 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -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"]
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 73b649b4d14..bf478b68765 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -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:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 7c4ba3878f1..ad516aba7c3 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -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}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d57be10f472..908dc730aa4 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -36,6 +36,7 @@ const Api = {
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
+ releasePath: '/api/:version/projects/:id/releases/:tag_name',
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: 'api/:version/application/statistics',
@@ -391,6 +392,22 @@ const Api = {
return axios.get(url);
},
+ release(projectPath, tagName) {
+ const url = Api.buildUrl(this.releasePath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.get(url);
+ },
+
+ updateRelease(projectPath, tagName, release) {
+ const url = Api.buildUrl(this.releasePath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':tag_name', encodeURIComponent(tagName));
+
+ return axios.put(url, release);
+ },
+
adminStatistics() {
const url = Api.buildUrl(this.adminStatisticsPath);
return axios.get(url);
diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js
new file mode 100644
index 00000000000..98ec196fc37
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/releases/edit/index.js
@@ -0,0 +1,7 @@
+import ZenMode from '~/zen_mode';
+import initEditRelease from '~/releases/detail';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ZenMode(); // eslint-disable-line no-new
+ initEditRelease();
+});
diff --git a/app/assets/javascripts/releases/detail/components/app.vue b/app/assets/javascripts/releases/detail/components/app.vue
new file mode 100644
index 00000000000..54a441de886
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/components/app.vue
@@ -0,0 +1,156 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+
+export default {
+ name: 'ReleaseDetailApp',
+ components: {
+ GlFormInput,
+ GlFormGroup,
+ GlButton,
+ MarkdownField,
+ },
+ directives: {
+ autofocusonshow,
+ },
+ computed: {
+ ...mapState([
+ 'isFetchingRelease',
+ 'fetchError',
+ 'markdownDocsPath',
+ 'markdownPreviewPath',
+ 'releasesPagePath',
+ ]),
+ showForm() {
+ return !this.isFetchingRelease && !this.fetchError;
+ },
+ subtitleText() {
+ return sprintf(
+ __(
+ 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}.',
+ ),
+ {
+ codeStart: '<code>',
+ codeEnd: '</code>',
+ },
+ false,
+ );
+ },
+ tagName() {
+ return this.$store.state.release.tagName;
+ },
+ releaseTitle: {
+ get() {
+ return this.$store.state.release.name;
+ },
+ set(title) {
+ this.updateReleaseTitle(title);
+ },
+ },
+ releaseNotes: {
+ get() {
+ return this.$store.state.release.description;
+ },
+ set(notes) {
+ this.updateReleaseNotes(notes);
+ },
+ },
+ },
+ created() {
+ this.fetchRelease();
+ },
+ methods: {
+ ...mapActions([
+ 'fetchRelease',
+ 'updateRelease',
+ 'updateReleaseTitle',
+ 'updateReleaseNotes',
+ 'navigateToReleasesPage',
+ ]),
+ },
+};
+</script>
+<template>
+ <div class="d-flex flex-column">
+ <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
+ <form v-if="showForm" @submit.prevent="updateRelease()">
+ <div class="row">
+ <gl-form-group class="col-md-6 col-lg-5 col-xl-4">
+ <label for="git-ref">{{ __('Tag name') }}</label>
+ <gl-form-input
+ id="git-ref"
+ v-model="tagName"
+ type="text"
+ class="form-control"
+ aria-describedby="tag-name-help"
+ disabled
+ />
+ <div id="tag-name-help" class="form-text text-muted">
+ {{ __('Choose an existing tag, or create a new one') }}
+ </div>
+ </gl-form-group>
+ </div>
+ <gl-form-group>
+ <label for="release-title">{{ __('Release title') }}</label>
+ <gl-form-input
+ id="release-title"
+ ref="releaseTitleInput"
+ v-model="releaseTitle"
+ v-autofocusonshow
+ autofocus
+ type="text"
+ class="form-control"
+ />
+ </gl-form-group>
+ <gl-form-group>
+ <label for="release-notes">{{ __('Release notes') }}</label>
+ <div class="bordered-box pr-3 pl-3">
+ <markdown-field
+ :can-attach-file="true"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :add-spacing-classes="false"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <textarea
+ id="release-notes"
+ slot="textarea"
+ v-model="releaseNotes"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="__('Release notes')"
+ :placeholder="__('Write your release notes or drag your files here…')"
+ @keydown.meta.enter="updateRelease()"
+ @keydown.ctrl.enter="updateRelease()"
+ >
+ </textarea>
+ </markdown-field>
+ </div>
+ </gl-form-group>
+
+ <div class="d-flex pt-3">
+ <gl-button
+ class="mr-auto js-submit-button"
+ variant="success"
+ type="submit"
+ :aria-label="__('Save changes')"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <gl-button
+ class="js-cancel-button"
+ variant="default"
+ type="button"
+ :aria-label="__('Cancel')"
+ @click="navigateToReleasesPage()"
+ >
+ {{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/detail/index.js b/app/assets/javascripts/releases/detail/index.js
new file mode 100644
index 00000000000..3da971e6d90
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import ReleaseDetailApp from './components/app.vue';
+import createStore from './store';
+
+export default () => {
+ const el = document.getElementById('js-edit-release-page');
+
+ const store = createStore(el.dataset);
+ store.dispatch('setInitialState', el.dataset);
+
+ return new Vue({
+ el,
+ store,
+ components: { ReleaseDetailApp },
+ render(createElement) {
+ return createElement('release-detail-app');
+ },
+ });
+};
diff --git a/app/assets/javascripts/releases/detail/store/actions.js b/app/assets/javascripts/releases/detail/store/actions.js
new file mode 100644
index 00000000000..c9749582f5c
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/actions.js
@@ -0,0 +1,62 @@
+import * as types from './mutation_types';
+import api from '~/api';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+export const setInitialState = ({ commit }, initialState) =>
+ commit(types.SET_INITIAL_STATE, initialState);
+
+export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
+export const receiveReleaseSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_RELEASE_SUCCESS, data);
+export const receiveReleaseError = ({ commit }, error) => {
+ commit(types.RECEIVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while getting the release details'));
+};
+
+export const fetchRelease = ({ dispatch, state }) => {
+ dispatch('requestRelease');
+
+ return api
+ .release(state.projectId, state.tagName)
+ .then(({ data: release }) => {
+ const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true });
+ dispatch('receiveReleaseSuccess', camelCasedRelease);
+ })
+ .catch(error => {
+ dispatch('receiveReleaseError', error);
+ });
+};
+
+export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
+export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+
+export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
+export const receiveUpdateReleaseSuccess = ({ commit, dispatch }) => {
+ commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
+ dispatch('navigateToReleasesPage');
+};
+export const receiveUpdateReleaseError = ({ commit }, error) => {
+ commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while saving the release details'));
+};
+
+export const updateRelease = ({ dispatch, state }) => {
+ dispatch('requestUpdateRelease');
+
+ return api
+ .updateRelease(state.projectId, state.tagName, {
+ name: state.release.name,
+ description: state.release.description,
+ })
+ .then(() => dispatch('receiveUpdateReleaseSuccess'))
+ .catch(error => {
+ dispatch('receiveUpdateReleaseError', error);
+ });
+};
+
+export const navigateToReleasesPage = ({ state }) => {
+ redirectTo(state.releasesPagePath);
+};
diff --git a/app/assets/javascripts/releases/detail/store/index.js b/app/assets/javascripts/releases/detail/store/index.js
new file mode 100644
index 00000000000..e8623a49356
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state,
+ });
diff --git a/app/assets/javascripts/releases/detail/store/mutation_types.js b/app/assets/javascripts/releases/detail/store/mutation_types.js
new file mode 100644
index 00000000000..75e1d78a645
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/mutation_types.js
@@ -0,0 +1,12 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+export const REQUEST_RELEASE = 'REQUEST_RELEASE';
+export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
+export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
+
+export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
+export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
+
+export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
+export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
+export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
diff --git a/app/assets/javascripts/releases/detail/store/mutations.js b/app/assets/javascripts/releases/detail/store/mutations.js
new file mode 100644
index 00000000000..d739978d755
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/mutations.js
@@ -0,0 +1,42 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_INITIAL_STATE](state, initialState) {
+ Object.keys(state).forEach(key => {
+ state[key] = initialState[key];
+ });
+ },
+
+ [types.REQUEST_RELEASE](state) {
+ state.isFetchingRelease = true;
+ },
+ [types.RECEIVE_RELEASE_SUCCESS](state, data) {
+ state.fetchError = undefined;
+ state.isFetchingRelease = false;
+ state.release = data;
+ },
+ [types.RECEIVE_RELEASE_ERROR](state, error) {
+ state.fetchError = error;
+ state.isFetchingRelease = false;
+ state.release = undefined;
+ },
+
+ [types.UPDATE_RELEASE_TITLE](state, title) {
+ state.release.name = title;
+ },
+ [types.UPDATE_RELEASE_NOTES](state, notes) {
+ state.release.description = notes;
+ },
+
+ [types.REQUEST_UPDATE_RELEASE](state) {
+ state.isUpdatingRelease = true;
+ },
+ [types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) {
+ state.updateError = undefined;
+ state.isUpdatingRelease = false;
+ },
+ [types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) {
+ state.updateError = error;
+ state.isUpdatingRelease = false;
+ },
+};
diff --git a/app/assets/javascripts/releases/detail/store/state.js b/app/assets/javascripts/releases/detail/store/state.js
new file mode 100644
index 00000000000..ff98e2bed78
--- /dev/null
+++ b/app/assets/javascripts/releases/detail/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+ projectId: null,
+ tagName: null,
+ releasesPagePath: null,
+ markdownDocsPath: null,
+ markdownPreviewPath: null,
+
+ release: null,
+
+ isFetchingRelease: false,
+ fetchError: null,
+
+ isUpdatingRelease: false,
+ updateError: null,
+});
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index c73db2668ec..ecd32dcd0ce 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -123,7 +123,7 @@ ul.content-list {
font-weight: $gl-font-weight-bold;
}
- a:not(.default-link-color) {
+ a {
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
new file mode 100644
index 00000000000..a6d30522ff7
--- /dev/null
+++ b/app/assets/stylesheets/pages/tags.scss
@@ -0,0 +1,3 @@
+.tag-release-link {
+ color: $blue-600 !important;
+}
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index d88ec06a18b..efd5f0fc607 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -4,18 +4,31 @@ class HealthController < ActionController::Base
protect_from_forgery with: :exception, prepend: true
include RequiresWhitelistedMonitoringClient
+ CHECKS = [
+ Gitlab::HealthChecks::DbCheck,
+ Gitlab::HealthChecks::Redis::RedisCheck,
+ Gitlab::HealthChecks::Redis::CacheCheck,
+ Gitlab::HealthChecks::Redis::QueuesCheck,
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
+ ].freeze
+
def readiness
- render_probe(::Gitlab::HealthChecks::Probes::Readiness)
+ # readiness check is a collection with all above application-level checks
+ render_checks(*CHECKS)
end
def liveness
- render_probe(::Gitlab::HealthChecks::Probes::Liveness)
+ # liveness check is a collection without additional checks
+ render_checks
end
private
- def render_probe(probe_class)
- result = probe_class.new.execute
+ def render_checks(*checks)
+ result = Gitlab::HealthChecks::Probes::Collection
+ .new(*checks)
+ .execute
# disable static error pages at the gitlab-workhorse level, we want to see this error response even in production
headers["X-GitLab-Custom-Error"] = 1 unless result.success?
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
index 32111b07a0b..766e2f86ea2 100644
--- a/app/controllers/projects/deployments_controller.rb
+++ b/app/controllers/projects/deployments_controller.rb
@@ -47,11 +47,9 @@ class Projects::DeploymentsController < Projects::ApplicationController
@deployment_metrics ||= DeploymentMetrics.new(deployment.project, deployment)
end
- # rubocop: disable CodeReuse/ActiveRecord
def deployment
- @deployment ||= environment.deployments.find_by(iid: params[:id])
+ @deployment ||= environment.deployments.find_successful_deployment!(params[:id])
end
- # rubocop: enable CodeReuse/ActiveRecord
def environment
@environment ||= project.environments.find(params[:environment_id])
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 1913d7cd580..4a37dfe5c19 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -51,9 +51,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
- note_positions = renderable_notes.map(&:position).compact
- @diffs.unfold_diff_files(note_positions)
-
+ @diffs.unfold_diff_files(note_positions.unfoldable)
@diffs.write_cache
request = {
@@ -140,6 +138,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
end
+ def note_positions
+ @note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position))
+ end
+
def renderable_notes
define_diff_comment_vars unless @notes
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 42fe42398f1..3c70ff3b59f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -289,7 +289,8 @@ module ApplicationSettingsHelper
:snowplow_collector_hostname,
:snowplow_cookie_domain,
:snowplow_enabled,
- :snowplow_site_id
+ :snowplow_site_id,
+ :push_event_hooks_limit
]
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 2b7320817ed..52f189b122f 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -18,12 +18,16 @@ module EnvironmentHelper
end
end
+ def deployment_path(deployment)
+ [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ end
+
def deployment_link(deployment, text: nil)
return unless deployment
link_label = text ? text : "##{deployment.iid}"
- link_to link_label, [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ link_to link_label, deployment_path(deployment)
end
def last_deployment_link_for_environment_build(project, build)
@@ -32,4 +36,31 @@ module EnvironmentHelper
deployment_link(environment.last_deployment)
end
+
+ def render_deployment_status(deployment)
+ status = deployment.status
+
+ status_text =
+ case status
+ when 'created'
+ s_('Deployment|created')
+ when 'running'
+ s_('Deployment|running')
+ when 'success'
+ s_('Deployment|success')
+ when 'failed'
+ s_('Deployment|failed')
+ when 'canceled'
+ s_('Deployment|canceled')
+ end
+
+ klass = "ci-status ci-#{status.dasherize}"
+ text = "#{ci_icon_for_status(status)} #{status_text}".html_safe
+
+ if deployment.deployable
+ link_to(text, deployment_path(deployment), class: klass)
+ else
+ content_tag(:span, text, class: klass)
+ end
+ end
end
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 3186bbd9322..68a19152d8f 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -19,4 +19,14 @@ module ReleasesHelper
documentation_path: help_page
}
end
+
+ def data_for_edit_release_page
+ {
+ project_id: @project.id,
+ tag_name: @release.tag,
+ markdown_preview_path: preview_markdown_path(@project),
+ markdown_docs_path: help_page_path('user/markdown'),
+ releases_page_path: project_releases_path(@project, anchor: @release.tag)
+ }
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 02f214341fb..0724ee8f39d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -214,6 +214,9 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
allow_nil: false
+ validates :push_event_hooks_limit,
+ numericality: { greater_than_or_equal_to: 0 }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index e2579316fdd..e9aab4a3d05 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -82,6 +82,7 @@ module ApplicationSettingImplementation
polling_interval_multiplier: 1,
project_export_enabled: true,
protected_ci_variables: false,
+ push_event_hooks_limit: 3,
raw_blob_request_limit: 300,
recaptcha_enabled: false,
login_recaptcha_protection_enabled: false,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 30694313f7a..7ccd5e98360 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -9,7 +9,7 @@ class Deployment < ApplicationRecord
belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user
- belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) do
Deployment.where(project: s.project).maximum(:iid) if s&.project
@@ -22,6 +22,8 @@ class Deployment < ApplicationRecord
scope :for_environment, -> (environment) { where(environment_id: environment) }
+ scope :visible, -> { where(status: %i[running success failed canceled]) }
+
state_machine :status, initial: :created do
event :run do
transition created: :running
@@ -73,6 +75,10 @@ class Deployment < ApplicationRecord
find(ids)
end
+ def self.find_successful_deployment!(iid)
+ success.find_by!(iid: iid)
+ end
+
def commit
project.commit(sha)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index fe438b142b2..af0c219d9a0 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,7 +6,8 @@ class Environment < ApplicationRecord
belongs_to :project, required: true
- has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
@@ -81,6 +82,10 @@ class Environment < ApplicationRecord
pluck(:name)
end
+ def self.find_or_create_by_name(name)
+ find_or_create_by(name: name)
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_ENVIRONMENT_NAME', value: name)
diff --git a/app/models/project.rb b/app/models/project.rb
index 4d518862146..f1e232e95f8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -281,7 +281,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
- has_many :deployments, -> { success }
+ has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
diff --git a/app/policies/deployment_policy.rb b/app/policies/deployment_policy.rb
index d4f2f3c52b1..1a92b735e36 100644
--- a/app/policies/deployment_policy.rb
+++ b/app/policies/deployment_policy.rb
@@ -7,8 +7,20 @@ class DeploymentPolicy < BasePolicy
can?(:update_build, @subject.deployable)
end
- rule { ~can_retry_deployable }.policy do
+ condition(:has_deployable) do
+ @subject.deployable.present?
+ end
+
+ condition(:can_update_deployment) do
+ can?(:update_deployment, @subject.environment)
+ end
+
+ rule { has_deployable & ~can_retry_deployable }.policy do
prevent :create_deployment
prevent :update_deployment
end
+
+ rule { ~can_update_deployment }.policy do
+ prevent :update_deployment
+ end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a3540f31077..ea2be37d7e6 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_container_image
enable :create_environment
enable :create_deployment
+ enable :update_deployment
enable :create_release
enable :update_release
end
diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb
new file mode 100644
index 00000000000..2572802e6a1
--- /dev/null
+++ b/app/services/deployments/after_create_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Deployments
+ class AfterCreateService
+ attr_reader :deployment
+ attr_reader :deployable
+
+ delegate :environment, to: :deployment
+ delegate :variables, to: :deployable
+ delegate :options, to: :deployable, allow_nil: true
+
+ def initialize(deployment)
+ @deployment = deployment
+ @deployable = deployment.deployable
+ end
+
+ def execute
+ deployment.create_ref
+ deployment.invalidate_cache
+
+ update_environment(deployment)
+
+ deployment
+ end
+
+ def update_environment(deployment)
+ ActiveRecord::Base.transaction do
+ if (url = expanded_environment_url)
+ environment.external_url = url
+ end
+
+ environment.fire_state_event(action)
+
+ if environment.save && !environment.stopped?
+ deployment.update_merge_request_metrics!
+ end
+ end
+ end
+
+ private
+
+ def environment_options
+ options&.dig(:environment) || {}
+ end
+
+ def expanded_environment_url
+ ExpandVariables.expand(environment_url, -> { variables }) if environment_url
+ end
+
+ def environment_url
+ environment_options[:url]
+ end
+
+ def action
+ environment_options[:action] || 'start'
+ end
+ end
+end
+
+Deployments::AfterCreateService.prepend_if_ee('EE::Deployments::AfterCreateService')
diff --git a/app/services/deployments/create_service.rb b/app/services/deployments/create_service.rb
new file mode 100644
index 00000000000..89e3f7c8b83
--- /dev/null
+++ b/app/services/deployments/create_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Deployments
+ class CreateService
+ attr_reader :environment, :current_user, :params
+
+ def initialize(environment, current_user, params)
+ @environment = environment
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ create_deployment.tap do |deployment|
+ AfterCreateService.new(deployment).execute if deployment.persisted?
+ end
+ end
+
+ def create_deployment
+ environment.deployments.create(deployment_attributes)
+ end
+
+ def deployment_attributes
+ # We use explicit parameters here so we never by accident allow parameters
+ # to be set that one should not be able to set (e.g. the row ID).
+ {
+ cluster_id: environment.deployment_platform&.cluster_id,
+ project_id: environment.project_id,
+ environment_id: environment.id,
+ ref: params[:ref],
+ tag: params[:tag],
+ sha: params[:sha],
+ user: current_user,
+ on_stop: params[:on_stop],
+ status: params[:status]
+ }
+ end
+ end
+end
diff --git a/app/services/deployments/update_service.rb b/app/services/deployments/update_service.rb
new file mode 100644
index 00000000000..7c8215d28f2
--- /dev/null
+++ b/app/services/deployments/update_service.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Deployments
+ class UpdateService
+ attr_reader :deployment, :params
+
+ def initialize(deployment, params)
+ @deployment = deployment
+ @params = params
+ end
+
+ def execute
+ deployment.update(status: params[:status])
+ end
+ end
+end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 97047d96de1..b1faef58e33 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -62,6 +62,8 @@ module Git
end
def execute_project_hooks
+ return unless params.fetch(:execute_project_hooks, true)
+
# Creating push_data invokes one CommitDelta RPC per commit. Only
# build this data if we actually need it.
project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name)
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index 33925147750..62159d4e7e5 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -17,7 +17,7 @@ module Git
changes_by_action = group_changes_by_action(changes)
changes_by_action.each do |_, changes|
- process_changes(ref_type, changes) if changes.any?
+ process_changes(ref_type, changes, execute_project_hooks: execute_project_hooks?(changes)) if changes.any?
end
end
@@ -34,7 +34,11 @@ module Git
:pushed
end
- def process_changes(ref_type, changes)
+ def execute_project_hooks?(changes)
+ (changes.size <= Gitlab::CurrentSettings.push_event_hooks_limit) || Feature.enabled?(:git_push_execute_all_project_hooks, project)
+ end
+
+ def process_changes(ref_type, changes, execute_project_hooks:)
push_service_class = push_service_class_for(ref_type)
changes.each do |change|
@@ -43,7 +47,8 @@ module Git
current_user,
change: change,
push_options: params[:push_options],
- create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
+ create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project),
+ execute_project_hooks: execute_project_hooks
).execute
end
end
diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb
deleted file mode 100644
index 730210c611a..00000000000
--- a/app/services/update_deployment_service.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-class UpdateDeploymentService
- attr_reader :deployment
- attr_reader :deployable
-
- delegate :environment, to: :deployment
- delegate :variables, to: :deployable
-
- def initialize(deployment)
- @deployment = deployment
- @deployable = deployment.deployable
- end
-
- def execute
- deployment.create_ref
- deployment.invalidate_cache
-
- ActiveRecord::Base.transaction do
- environment.external_url = expanded_environment_url if
- expanded_environment_url
-
- environment.fire_state_event(action)
-
- break unless environment.save
- break if environment.stopped?
-
- deployment.tap(&:update_merge_request_metrics!)
- end
-
- deployment
- end
-
- private
-
- def environment_options
- @environment_options ||= deployable.options&.dig(:environment) || {}
- end
-
- def expanded_environment_url
- return @expanded_environment_url if defined?(@expanded_environment_url)
- return unless environment_url
-
- @expanded_environment_url =
- ExpandVariables.expand(environment_url, -> { variables })
- end
-
- def environment_url
- environment_options[:url]
- end
-
- def action
- environment_options[:action] || 'start'
- end
-end
-
-UpdateDeploymentService.prepend_if_ee('EE::UpdateDeploymentService')
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index b52171afc69..22458223b93 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -20,5 +20,10 @@
= f.number_field :raw_blob_request_limit, class: 'form-control'
.form-text.text-muted
= _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.')
+ .form-group
+ = f.label :push_event_hooks_limit, class: 'label-bold'
+ = f.number_field :push_event_hooks_limit, class: 'form-control'
+ .form-text.text-muted
+ = _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 688b8f001c3..7c73bbc7479 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title "Repository"
- page_title @blob.path, @ref
-- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit)
+- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
.js-signature-container{ data: { 'signatures-path': signatures_path } }
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index ef2ab4c698e..8270477ed3f 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -1,31 +1,49 @@
.gl-responsive-table-row.deployment{ role: 'row' }
+ .table-section.section-15{ role: 'gridcell' }
+ .table-mobile-header{ role: 'rowheader' }= _("Status")
+ .table-mobile-content
+ = render_deployment_status(deployment)
+
.table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("ID")
%strong.table-mobile-content ##{deployment.iid}
- .table-section.section-30{ role: 'gridcell' }
+ .table-section.section-10{ role: 'gridcell' }
+ .table-mobile-header{ role: 'rowheader' }= _("Triggerer")
+ .table-mobile-content
+ - if deployment.deployed_by
+ = user_avatar(user: deployment.deployed_by, size: 26, css_class: "mr-0 float-none")
+
+ .table-section.section-25{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Commit")
= render 'projects/deployments/commit', deployment: deployment
- .table-section.section-25.build-column{ role: 'gridcell' }
+ .table-section.section-10.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Job")
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
.flex-truncate-child
- = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
+ = link_to deployment_path(deployment), class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id})
- - if deployment.deployed_by
- %div
- by
- = user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none")
+ - else
+ .badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
+ = s_('Deployment|API')
- .table-section.section-15{ role: 'gridcell' }
+ .table-section.section-10{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' }= _("Created")
+ %span.table-mobile-content.flex-truncate-parent
+ %span.flex-truncate-child
+ = time_ago_with_tooltip(deployment.created_at)
+
+ .table-section.section-10{ role: 'gridcell' }
+ .table-mobile-header{ role: 'rowheader' }= _("Deployed")
- if deployment.deployed_at
- %span.table-mobile-content= time_ago_with_tooltip(deployment.deployed_at)
+ %span.table-mobile-content.flex-truncate-parent
+ %span.flex-truncate-child
+ = time_ago_with_tooltip(deployment.deployed_at)
- .table-section.section-20.table-button-footer{ role: 'gridcell' }
+ .table-section.section-10.table-button-footer{ role: 'gridcell' }
.btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index d6bf8d564de..dffa5e4ba40 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, deployment)
+- if deployment.deployable && can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= button_tag class: 'btn btn-default btn-build has-tooltip', type: 'button', data: { toggle: 'modal', target: "#confirm-rollback-modal-#{deployment.id}" }, title: tooltip do
- if deployment.last?
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 75da151f329..c4c39c227c6 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -60,10 +60,13 @@
.table-holder
.ci-table.environments{ role: 'grid' }
.gl-responsive-table-row.table-row-header{ role: 'row' }
+ .table-section.section-15{ role: 'columnheader' }= _('Status')
.table-section.section-10{ role: 'columnheader' }= _('ID')
- .table-section.section-30{ role: 'columnheader' }= _('Commit')
- .table-section.section-25{ role: 'columnheader' }= _('Job')
- .table-section.section-15{ role: 'columnheader' }= _('Created')
+ .table-section.section-10{ role: 'columnheader' }= _('Triggerer')
+ .table-section.section-25{ role: 'columnheader' }= _('Commit')
+ .table-section.section-10{ role: 'columnheader' }= _('Job')
+ .table-section.section-10{ role: 'columnheader' }= _('Created')
+ .table-section.section-10{ role: 'columnheader' }= _('Deployed')
= render @deployments
diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml
index acc2c50294f..fe89d2fb748 100644
--- a/app/views/projects/issues/import_csv/_button.html.haml
+++ b/app/views/projects/issues/import_csv/_button.html.haml
@@ -3,7 +3,7 @@
%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
data: { toggle: 'modal', target: '.issues-import-modal' } }
- if type == :icon
- = sprite_icon('upload')
+ = sprite_icon('import')
- else
= _('Import CSV')
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
new file mode 100644
index 00000000000..88ca64f2af0
--- /dev/null
+++ b/app/views/projects/releases/edit.html.haml
@@ -0,0 +1,3 @@
+- page_title _('Edit Release')
+
+#js-edit-release-page{ data: data_for_edit_release_page }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 3f6cd628d64..c7bd0262c54 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -24,7 +24,7 @@
.text-secondary
= icon('rocket')
= _("Release")
- = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'default-link-color'
+ = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'tag-release-link'
- if release.description.present?
.description.md.prepend-top-default
= markdown_field(release, :description)
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index 83f60fa6fe2..4fed95e2607 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,4 +1,4 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
- = icon('rss')
+= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
+ = sprite_icon('rss')
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
- = custom_icon('icon_calendar')
+ = sprite_icon('calendar')
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
index da517f3fb26..3c7e384365a 100644
--- a/app/workers/deployments/success_worker.rb
+++ b/app/workers/deployments/success_worker.rb
@@ -10,7 +10,7 @@ module Deployments
Deployment.find_by_id(deployment_id).try do |deployment|
break unless deployment.success?
- UpdateDeploymentService.new(deployment).execute
+ Deployments::AfterCreateService.new(deployment).execute
end
end
end
diff --git a/changelogs/unreleased/27715-fix-unrenderable-notes.yml b/changelogs/unreleased/27715-fix-unrenderable-notes.yml
new file mode 100644
index 00000000000..329f9cbb30c
--- /dev/null
+++ b/changelogs/unreleased/27715-fix-unrenderable-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix showing diff when it has legacy diff notes
+merge_request: 18510
+author:
+type: fixed
diff --git a/changelogs/unreleased/31009-limit-project-hooks-services.yml b/changelogs/unreleased/31009-limit-project-hooks-services.yml
new file mode 100644
index 00000000000..dc1e0461567
--- /dev/null
+++ b/changelogs/unreleased/31009-limit-project-hooks-services.yml
@@ -0,0 +1,5 @@
+---
+title: Don't execute webhooks/services when above limit
+merge_request: 17874
+author:
+type: performance
diff --git a/changelogs/unreleased/32380-update-issue-list-icons.yml b/changelogs/unreleased/32380-update-issue-list-icons.yml
new file mode 100644
index 00000000000..42ad9b1eb99
--- /dev/null
+++ b/changelogs/unreleased/32380-update-issue-list-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Use correct icons for issue actions
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/add-ansi2json-log-parser.yml b/changelogs/unreleased/add-ansi2json-log-parser.yml
new file mode 100644
index 00000000000..1aec5d36fbe
--- /dev/null
+++ b/changelogs/unreleased/add-ansi2json-log-parser.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce new Ansi2json parser to convert job logs to JSON
+merge_request: 18133
+author:
+type: added
diff --git a/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml b/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml
new file mode 100644
index 00000000000..f266d197c6c
--- /dev/null
+++ b/changelogs/unreleased/allow-api-lookup-of-inherited-member-by-id.yml
@@ -0,0 +1,5 @@
+---
+title: Add individual inherited member lookup API
+merge_request: 17744
+author:
+type: added
diff --git a/changelogs/unreleased/deployments-api.yml b/changelogs/unreleased/deployments-api.yml
new file mode 100644
index 00000000000..dce1763bdf1
--- /dev/null
+++ b/changelogs/unreleased/deployments-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add API for manually creating and updating deployments
+merge_request: 17620
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-edit-release-page.yml b/changelogs/unreleased/nfriend-add-edit-release-page.yml
new file mode 100644
index 00000000000..5369ab6b19c
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-edit-release-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add "Edit Release" page
+merge_request: 18033
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-fix-lin.yml b/changelogs/unreleased/nfriend-fix-lin.yml
new file mode 100644
index 00000000000..0b16eb9c1f4
--- /dev/null
+++ b/changelogs/unreleased/nfriend-fix-lin.yml
@@ -0,0 +1,5 @@
+---
+title: Fix button link foreground color
+merge_request: 18669
+author:
+type: fixed
diff --git a/config/helpers/is_ee_env.js b/config/helpers/is_ee_env.js
index 801cf6abc81..78f0bd65528 100644
--- a/config/helpers/is_ee_env.js
+++ b/config/helpers/is_ee_env.js
@@ -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;
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b5656040da2..25fb6cc5f5a 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -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),
}),
diff --git a/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb b/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
new file mode 100644
index 00000000000..f107181bbde
--- /dev/null
+++ b/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index f3ec5658f33..ab01ba21ee0 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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"
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index fc2986380f3..de8c3336335 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -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.
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index df3a98b1dc8..27254c42e3a 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -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
+}
+```
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1efda2f07eb..b21fc9bfb18 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/api/members.md b/doc/api/members.md
index da62dc53659..50dcf86c972 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -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.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index efb6809794f..24e6f90e844 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -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. |
diff --git a/doc/development/chatops_on_gitlabcom.md b/doc/development/chatops_on_gitlabcom.md
index a1c07ee2a1e..8a313a120f1 100644
--- a/doc/development/chatops_on_gitlabcom.md
+++ b/doc/development/chatops_on_gitlabcom.md
@@ -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
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 988f82118cb..efff477eff6 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -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:
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index f89371f38ce..cc9df479492 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -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
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 6520c7dbbcf..5954de03db4 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -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
diff --git a/doc/user/group/epics/img/child_epics_roadmap.png b/doc/user/group/epics/img/child_epics_roadmap.png
deleted file mode 100644
index 819fed58989..00000000000
--- a/doc/user/group/epics/img/child_epics_roadmap.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view.png b/doc/user/group/epics/img/epic_view.png
deleted file mode 100644
index c55d302ec29..00000000000
--- a/doc/user/group/epics/img/epic_view.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
new file mode 100755
index 00000000000..a17c56c618b
--- /dev/null
+++ b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view_v12.3.png b/doc/user/group/epics/img/epic_view_v12.3.png
new file mode 100755
index 00000000000..79758cf3d52
--- /dev/null
+++ b/doc/user/group/epics/img/epic_view_v12.3.png
Binary files differ
diff --git a/doc/user/group/epics/img/epics_list_view.png b/doc/user/group/epics/img/epics_list_view.png
deleted file mode 100644
index b30608d9d31..00000000000
--- a/doc/user/group/epics/img/epics_list_view.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/epics_list_view_v12.3.png b/doc/user/group/epics/img/epics_list_view_v12.3.png
new file mode 100755
index 00000000000..c6817a503e7
--- /dev/null
+++ b/doc/user/group/epics/img/epics_list_view_v12.3.png
Binary files differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 51e779cce6a..f9690d4edfe 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -10,13 +10,13 @@ Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
milestones.
-![epics list view](img/epics_list_view.png)
+![epics list view](img/epics_list_view_v12.3.png)
## Use cases
- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
- Track when the work for the group of issues is targeted to begin, and when it is targeted to end.
-- Discuss and collaborate on feature ideas and scope at a high-level.
+- Discuss and collaborate on feature ideas and scope at a high level.
## Creating an epic
@@ -24,78 +24,114 @@ A paginated list of epics is available in each group from where you can create
a new epic. The list of epics includes also epics from all subgroups of the
selected group. From your group page:
-1. Go to **Epics**
-1. Click the **New epic** button at the top right
-1. Enter a descriptive title and hit **Create epic**
+1. Go to **Epics**.
+1. Click **New epic**.
+1. Enter a descriptive title and click **Create epic**.
-Once created, you will be taken to the view for that newly-created epic where
-you can change its title, description, start date, and due date.
+You will be taken to the new epic where can edit the following details:
-![epic view](img/epic_view.png)
+- Title
+- Description
+- Start date
+- Due date
+- Labels
+
+An epic's page contains the following tabs:
+
+- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
+ - Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
+- **Roadmap**: a roadmap view of child epics which have start and due dates.
+
+![epic view](img/epic_view_v12.3.png)
## Adding an issue to an epic
+Any issue that belongs to a project in the epic's group, or any of the epic's
+subgroups, are eligible to be added. New issues appear at the top of the list of issues in the **Epics and Issues** tab.
+
An epic contains a list of issues and an issue can be associated with at most
-one epic. When on an epic, you can add its associated issues:
+one epic. When you add an issue to an epic that is already associated with another epic,
+the issue is automatically removed from the previous epic.
+
+To add an issue to an epic:
-1. Click the plus icon (<kbd>+</kbd>) under the epic description.
-1. Paste the link of the issue (you can hit <kbd>Spacebar</kbd> to add more than
- one issues at a time).
+1. Click **Add an issue**.
+1. Paste the link of the issue.
+ - Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
1. Click **Add**.
-Any issue belonging to a project in the epic's group or any of the epic's
-subgroups are eligible to be added. To remove an issue from an epic, click
-on the <kbd>x</kbd> button in the epic's issue list.
+To remove an issue from an epic:
-NOTE: **Note:**
-When you add an issue or an epic to an epic that's already associated with another epic,
-the issue or the epic is automatically removed from the previous epic.
+1. Click on the <kbd>x</kbd> button in the epic's issue list.
+1. Click **Remove** in the **Remove issue** warning message.
## Multi-level child epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7.
-Much like adding issues to an epic, an epic can have multiple child epics with
-the maximum depth being 5. To add a child epic:
+Any epic that belongs to a group, or subgroup of the parent epic's group, is
+eligible to be added. New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
+
+When you add a child epic that is already associated with another epic,
+that epic is automatically removed from the previous epic.
-1. Click the plus icon (<kbd>+</kbd>) under the epic description.
+An epic can have multiple child epics with
+the maximum depth being 5.
+
+To add a child epic:
+
+1. Click **Add an epic**.
1. Paste the link of the epic.
+ - Press <kbd>Spacebar</kbd> and repeat this step if there are multiple issues.
1. Click **Add**.
-Any epic that belongs to a group or subgroup of the parent epic's group is
-eligible to be added. To remove a child epic from a parent epic,
-click on the <kbd>x</kbd> button in the parent epic's epic list.
+To remove a child epic from a parent epic:
+
+1. Click on the <kbd>x</kbd> button in the parent epic's list of epics.
+1. Click **Remove** in the **Remove epic** warning message.
## Start date and due date
-For each of the dates in the sidebar of an epic, you can choose to either:
+To set a **Start date** and **Due date** for an epic, you can choose either of the following:
-- Enter a fixed value.
-- Inherit a dynamic value called "From milestones".
+- **Fixed**: Enter a fixed value.
+- **From milestones:** Inherit a dynamic value from the issues added to the epic.
-If you select "From milestones" for the start date, GitLab will automatically set the
+If you select **From milestones** for the start date, GitLab will automatically set the
date to be earliest start date across all milestones that are currently assigned
-to the issues that are attached to the epic. Similarly, if you select "From milestones"
+to the issues that are added to the epic. Similarly, if you select "From milestones"
for the due date, GitLab will set it to be the latest due date across all
milestones that are currently assigned to those issues.
-These are dynamic dates in that if milestones are re-assigned to the issues, if the
-milestone dates change, or if issues are added or removed from the epic, then
-the re-calculation will happen immediately to set a new dynamic date.
+These are dynamic dates which are recalculated immediately if any of the following occur:
+
+- Milestones are re-assigned to the issues.
+- Milestone dates change.
+- Issues are added or removed from the epic.
-## Roadmap in epics
+## Roadmap
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
If your epic contains one or more [child epics](#multi-level-child-epics) which
-have a [start or due date](#start-date-and-due-date), then you can see a
-[roadmap](../roadmap/index.md) view of the child epics under the parent epic itself.
+have a [start or due date](#start-date-and-due-date), a
+[roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic.
-![Child epics roadmap](img/child_epics_roadmap.png)
+![Child epics roadmap](img/epic_view_roadmap_v12.3.png)
## Reordering issues and child epics
-Drag and drop to reorder issues and child epics. New issues and child epics added to an epic appear at the top of the list.
+New issues and child epics are added to the top of their respective lists in the **Epics and Issues** tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot be intermingled.
+
+To reorder issues assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop issues into the desired order.
+
+To reorder child epics assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop epics into the desired order.
## Updating epics
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index a72cd990706..bcd79bd04bf 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -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
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index 0c0b44b3cd8..8ed10c09891 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -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>
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 168ec1b15ea..e385ee53636 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -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
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 3daf94702e7..d0f538a4b52 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -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**:
```
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index eb45df31ff9..da882547071 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -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
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 461ffe71a62..1d4616fed52 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -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
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index e4ef507228b..b7a471f14fe 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -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'
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 0cc9a6a5fb1..ad8e693ccbc 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -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
diff --git a/lib/gitlab/ci/ansi2json.rb b/lib/gitlab/ci/ansi2json.rb
new file mode 100644
index 00000000000..79114d35916
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json.rb
@@ -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
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
new file mode 100644
index 00000000000..53adaf38b87
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -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 << '&lt;'
+ elsif scanner.scan(/\r?\n/)
+ # we advance the offset of the next current line
+ # so it does not start from \n
+ flush_current_line(advance_offset: scanner.matched_size)
+ else
+ @state.current_line << scanner.scan(/./m)
+ end
+
+ @state.offset += scanner.matched_size
+ end
+ end
+
+ def handle_sequence(scanner)
+ indicator = scanner[1]
+ commands = scanner[2].split ';'
+ terminator = scanner[3]
+
+ # We are only interested in color and text style changes - triggered by
+ # sequences starting with '\e[' and ending with 'm'. Any other control
+ # sequence gets stripped (including stuff like "delete last line")
+ return unless indicator == '[' && terminator == 'm'
+
+ @state.update_style(commands)
+ end
+
+ def handle_section(scanner)
+ action = scanner[1]
+ timestamp = scanner[2]
+ section = scanner[3]
+
+ section_name = sanitize_section_name(section)
+
+ if action == "start"
+ handle_section_start(section_name, timestamp)
+ elsif action == "end"
+ handle_section_end(section_name, timestamp)
+ end
+ end
+
+ def handle_section_start(section, timestamp)
+ flush_current_line unless @state.current_line.empty?
+ @state.open_section(section, timestamp)
+ end
+
+ def handle_section_end(section, timestamp)
+ return unless @state.section_open?(section)
+
+ flush_current_line unless @state.current_line.empty?
+ @state.close_section(section, timestamp)
+
+ # ensure that section end is detached from the last
+ # line in the section
+ flush_current_line
+ end
+
+ def flush_current_line(advance_offset: 0)
+ @lines << @state.current_line.to_h
+
+ @state.set_current_line!(advance_offset: advance_offset)
+ end
+
+ def sanitize_section_name(section)
+ section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
new file mode 100644
index 00000000000..173fb1df88e
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -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
diff --git a/lib/gitlab/ci/ansi2json/parser.rb b/lib/gitlab/ci/ansi2json/parser.rb
new file mode 100644
index 00000000000..d428680fb2a
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/parser.rb
@@ -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
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
new file mode 100644
index 00000000000..db7a9035b8b
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -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
diff --git a/lib/gitlab/ci/ansi2json/style.rb b/lib/gitlab/ci/ansi2json/style.rb
new file mode 100644
index 00000000000..2739ffdfa5d
--- /dev/null
+++ b/lib/gitlab/ci/ansi2json/style.rb
@@ -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
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
index 0691f3cd131..5ff8d881143 100644
--- a/lib/gitlab/cycle_analytics/summary/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -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
diff --git a/lib/gitlab/diff/position_collection.rb b/lib/gitlab/diff/position_collection.rb
index 59c60f77aaa..2112d347678 100644
--- a/lib/gitlab/diff/position_collection.rb
+++ b/lib/gitlab/diff/position_collection.rb
@@ -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
diff --git a/lib/gitlab/graphql/docs/renderer.rb b/lib/gitlab/graphql/docs/renderer.rb
index f47a372aa19..41aef64f683 100644
--- a/lib/gitlab/graphql/docs/renderer.rb
+++ b/lib/gitlab/graphql/docs/renderer.rb
@@ -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)
diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml
index cc22d43ab4f..33acff38ef4 100644
--- a/lib/gitlab/graphql/docs/templates/default.md.haml
+++ b/lib/gitlab/graphql/docs/templates/default.md.haml
@@ -20,6 +20,3 @@
- type[:fields].each do |field|
= "| `#{field[:name]}` | #{render_field_type(field[:type][:info])} | #{field[:description]} |"
\
-
-
-
diff --git a/lib/gitlab/health_checks/checks.rb b/lib/gitlab/health_checks/checks.rb
deleted file mode 100644
index c4016c5fffd..00000000000
--- a/lib/gitlab/health_checks/checks.rb
+++ /dev/null
@@ -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
diff --git a/lib/gitlab/health_checks/probes/readiness.rb b/lib/gitlab/health_checks/probes/collection.rb
index 28abf490ffc..db3ef4834c2 100644
--- a/lib/gitlab/health_checks/probes/readiness.rb
+++ b/lib/gitlab/health_checks/probes/collection.rb
@@ -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
diff --git a/lib/gitlab/health_checks/probes/liveness.rb b/lib/gitlab/health_checks/probes/liveness.rb
deleted file mode 100644
index b4d346e945e..00000000000
--- a/lib/gitlab/health_checks/probes/liveness.rb
+++ /dev/null
@@ -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
diff --git a/lib/gitlab/metrics/exporter/base_exporter.rb b/lib/gitlab/metrics/exporter/base_exporter.rb
index 01d1ec9305e..7111835c85a 100644
--- a/lib/gitlab/metrics/exporter/base_exporter.rb
+++ b/lib/gitlab/metrics/exporter/base_exporter.rb
@@ -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)
diff --git a/lib/gitlab/metrics/exporter/web_exporter.rb b/lib/gitlab/metrics/exporter/web_exporter.rb
index 597ac289193..3940f6fa155 100644
--- a/lib/gitlab/metrics/exporter/web_exporter.rb
+++ b/lib/gitlab/metrics/exporter/web_exporter.rb
@@ -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
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index fd8df015903..902f22684ee 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 40fb2194a14..01b42eddb28 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3066,6 +3066,9 @@ msgstr ""
msgid "Choose a type..."
msgstr ""
+msgid "Choose an existing tag, or create a new one"
+msgstr ""
+
msgid "Choose any color."
msgstr ""
@@ -5409,6 +5412,27 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment|API"
+msgstr ""
+
+msgid "Deployment|This deployment was created using the API"
+msgstr ""
+
+msgid "Deployment|canceled"
+msgstr ""
+
+msgid "Deployment|created"
+msgstr ""
+
+msgid "Deployment|failed"
+msgstr ""
+
+msgid "Deployment|running"
+msgstr ""
+
+msgid "Deployment|success"
+msgstr ""
+
msgid "Deprioritize label"
msgstr ""
@@ -5766,6 +5790,9 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
+msgid "Edit Release"
+msgstr ""
+
msgid "Edit Snippet"
msgstr ""
@@ -11172,6 +11199,9 @@ msgstr ""
msgid "Number of LOCs per commit"
msgstr ""
+msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
+msgstr ""
+
msgid "Number of commits per MR"
msgstr ""
@@ -13456,12 +13486,27 @@ msgstr ""
msgid "Release"
msgstr ""
+msgid "Release notes"
+msgstr ""
+
+msgid "Release title"
+msgstr ""
+
msgid "Releases"
msgstr ""
+msgid "Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}."
+msgstr ""
+
msgid "Releases mark specific points in a project's development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API."
msgstr ""
+msgid "Release|Something went wrong while getting the release details"
+msgstr ""
+
+msgid "Release|Something went wrong while saving the release details"
+msgstr ""
+
msgid "Remember me"
msgstr ""
@@ -15943,6 +15988,9 @@ msgstr ""
msgid "Tag list:"
msgstr ""
+msgid "Tag name"
+msgstr ""
+
msgid "Tag this commit."
msgstr ""
@@ -18683,6 +18731,9 @@ msgstr ""
msgid "Write milestone description..."
msgstr ""
+msgid "Write your release notes or drag your files here…"
+msgstr ""
+
msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
msgstr ""
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index b9ee69a617b..66112c95742 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -75,15 +75,13 @@ describe Projects::DeploymentsController do
}
end
- before do
+ it 'returns a metrics JSON document' do
expect_next_instance_of(DeploymentMetrics) do |deployment_metrics|
allow(deployment_metrics).to receive(:has_metrics?).and_return(true)
expect(deployment_metrics).to receive(:metrics).and_return(empty_metrics)
end
- end
- it 'returns a metrics JSON document' do
get :metrics, params: deployment_params(id: deployment.to_param)
expect(response).to be_ok
@@ -91,6 +89,19 @@ describe Projects::DeploymentsController do
expect(json_response['metrics']).to eq({})
expect(json_response['last_update']).to eq(42)
end
+
+ it 'returns a 404 if the deployment failed' do
+ failed_deployment = create(
+ :deployment,
+ :failed,
+ project: project,
+ environment: environment
+ )
+
+ get :metrics, params: deployment_params(id: failed_deployment.to_param)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index e677e836145..5c02e8d6461 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -82,9 +82,9 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'when note has no position' do
+ context 'when note is a legacy diff note' do
before do
- create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request, position: nil)
+ create(:legacy_diff_note_on_merge_request, project: project, noteable: merge_request)
end
it 'serializes merge request diff collection' do
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 25823b75d18..dd690699ff6 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -66,8 +66,8 @@ describe 'Environment' do
create(:deployment, :running, environment: environment, deployable: build)
end
- it 'does not show deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
end
@@ -79,8 +79,8 @@ describe 'Environment' do
create(:deployment, :failed, environment: environment, deployable: build)
end
- it 'does not show deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ it 'does show deployments' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
end
@@ -175,7 +175,7 @@ describe 'Environment' do
#
# In EE we have to stub EE::Environment since it overwrites
# the "terminals" method.
- allow_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ allow_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
.to receive(:terminals) { nil }
visit terminal_project_environment_path(project, environment)
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index b1e3c000ddf..0cfeadfe548 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -61,7 +61,7 @@
"type": "array",
"items": { "$ref": "job/job.json" }
},
- "status": { "type": "string" }
+ "status": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/frontend/releases/detail/components/app_spec.js b/spec/frontend/releases/detail/components/app_spec.js
new file mode 100644
index 00000000000..f8eb33a69a8
--- /dev/null
+++ b/spec/frontend/releases/detail/components/app_spec.js
@@ -0,0 +1,70 @@
+import Vuex from 'vuex';
+import { mount } from '@vue/test-utils';
+import ReleaseDetailApp from '~/releases/detail/components/app';
+import { release } from '../../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('Release detail component', () => {
+ let wrapper;
+ let releaseClone;
+ let actions;
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+
+ releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
+
+ const state = {
+ release: releaseClone,
+ markdownDocsPath: 'path/to/markdown/docs',
+ };
+
+ actions = {
+ fetchRelease: jest.fn(),
+ updateRelease: jest.fn(),
+ navigateToReleasesPage: jest.fn(),
+ };
+
+ const store = new Vuex.Store({ actions, state });
+
+ wrapper = mount(ReleaseDetailApp, { store });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('calls fetchRelease when the component is created', () => {
+ expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders the description text at the top of the page', () => {
+ expect(wrapper.find('.js-subtitle-text').text()).toBe(
+ 'Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example v1.0, v2.0-pre.',
+ );
+ });
+
+ it('renders the correct tag name in the "Tag name" field', () => {
+ expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
+ });
+
+ it('renders the correct release title in the "Release title" field', () => {
+ expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
+ });
+
+ it('renders the release notes in the "Release notes" textarea', () => {
+ expect(wrapper.find('#release-notes').element.value).toBe(releaseClone.description);
+ });
+
+ it('renders the "Save changes" button as type="submit"', () => {
+ expect(wrapper.find('.js-submit-button').attributes('type')).toBe('submit');
+ });
+
+ it('calls updateRelease when the form is submitted', () => {
+ wrapper.find('form').trigger('submit');
+ expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls navigateToReleasesPage when the "Cancel" button is clicked', () => {
+ wrapper.find('.js-cancel-button').vm.$emit('click');
+ expect(actions.navigateToReleasesPage).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/releases/detail/store/actions_spec.js b/spec/frontend/releases/detail/store/actions_spec.js
new file mode 100644
index 00000000000..f1c7f3c1048
--- /dev/null
+++ b/spec/frontend/releases/detail/store/actions_spec.js
@@ -0,0 +1,217 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import * as actions from '~/releases/detail/store/actions';
+import testAction from 'helpers/vuex_action_helper';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+import state from '~/releases/detail/store/state';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+jest.mock('~/flash', () => jest.fn());
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
+}));
+
+describe('Release detail actions', () => {
+ let stateClone;
+ let releaseClone;
+ let mock;
+ let error;
+
+ beforeEach(() => {
+ stateClone = state();
+ releaseClone = JSON.parse(JSON.stringify(release));
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+ error = { message: 'An error occurred' };
+ createFlash.mockClear();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setInitialState', () => {
+ it(`commits ${types.SET_INITIAL_STATE} with the provided object`, () => {
+ const initialState = {};
+
+ return testAction(actions.setInitialState, initialState, stateClone, [
+ { type: types.SET_INITIAL_STATE, payload: initialState },
+ ]);
+ });
+ });
+
+ describe('requestRelease', () => {
+ it(`commits ${types.REQUEST_RELEASE}`, () =>
+ testAction(actions.requestRelease, undefined, stateClone, [{ type: types.REQUEST_RELEASE }]));
+ });
+
+ describe('receiveReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
+ testAction(actions.receiveReleaseSuccess, releaseClone, stateClone, [
+ { type: types.RECEIVE_RELEASE_SUCCESS, payload: releaseClone },
+ ]));
+ });
+
+ describe('receiveReleaseError', () => {
+ it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
+ testAction(actions.receiveReleaseError, error, stateClone, [
+ { type: types.RECEIVE_RELEASE_ERROR, payload: error },
+ ]));
+
+ it('shows a flash with an error message', () => {
+ actions.receiveReleaseError({ commit: jest.fn() }, error);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while getting the release details',
+ );
+ });
+ });
+
+ describe('fetchRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ stateClone.projectId = '18';
+ stateClone.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ });
+
+ it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
+ mock.onGet(getReleaseUrl).replyOnce(200, releaseClone);
+
+ return testAction(
+ actions.fetchRelease,
+ undefined,
+ stateClone,
+ [],
+ [
+ { type: 'requestRelease' },
+ {
+ type: 'receiveReleaseSuccess',
+ payload: convertObjectPropsToCamelCase(releaseClone, { deep: true }),
+ },
+ ],
+ );
+ });
+
+ it(`dispatches requestRelease and receiveReleaseError with an error object`, () => {
+ mock.onGet(getReleaseUrl).replyOnce(500);
+
+ return testAction(
+ actions.fetchRelease,
+ undefined,
+ stateClone,
+ [],
+ [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
+ );
+ });
+ });
+
+ describe('updateReleaseTitle', () => {
+ it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
+ const newTitle = 'The new release title';
+ return testAction(actions.updateReleaseTitle, newTitle, stateClone, [
+ { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
+ ]);
+ });
+ });
+
+ describe('updateReleaseNotes', () => {
+ it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
+ const newReleaseNotes = 'The new release notes';
+ return testAction(actions.updateReleaseNotes, newReleaseNotes, stateClone, [
+ { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
+ ]);
+ });
+ });
+
+ describe('requestUpdateRelease', () => {
+ it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
+ testAction(actions.requestUpdateRelease, undefined, stateClone, [
+ { type: types.REQUEST_UPDATE_RELEASE },
+ ]));
+ });
+
+ describe('receiveUpdateReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
+ testAction(
+ actions.receiveUpdateReleaseSuccess,
+ undefined,
+ stateClone,
+ [{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS }],
+ [{ type: 'navigateToReleasesPage' }],
+ ));
+ });
+
+ describe('receiveUpdateReleaseError', () => {
+ it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
+ testAction(actions.receiveUpdateReleaseError, error, stateClone, [
+ { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
+ ]));
+
+ it('shows a flash with an error message', () => {
+ actions.receiveUpdateReleaseError({ commit: jest.fn() }, error);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while saving the release details',
+ );
+ });
+ });
+
+ describe('updateRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ stateClone.release = releaseClone;
+ stateClone.projectId = '18';
+ stateClone.tagName = 'v1.3';
+ getReleaseUrl = `/api/v4/projects/${stateClone.projectId}/releases/${stateClone.tagName}`;
+ });
+
+ it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
+ mock.onPut(getReleaseUrl).replyOnce(200);
+
+ return testAction(
+ actions.updateRelease,
+ undefined,
+ stateClone,
+ [],
+ [{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
+ );
+ });
+
+ it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => {
+ mock.onPut(getReleaseUrl).replyOnce(500);
+
+ return testAction(
+ actions.updateRelease,
+ undefined,
+ stateClone,
+ [],
+ [
+ { type: 'requestUpdateRelease' },
+ { type: 'receiveUpdateReleaseError', payload: expect.anything() },
+ ],
+ );
+ });
+ });
+
+ describe('navigateToReleasesPage', () => {
+ it(`calls redirectTo() with the URL to the releases page`, () => {
+ const releasesPagePath = 'path/to/releases/page';
+ stateClone.releasesPagePath = releasesPagePath;
+
+ actions.navigateToReleasesPage({ state: stateClone });
+
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(releasesPagePath);
+ });
+ });
+});
diff --git a/spec/frontend/releases/detail/store/mutations_spec.js b/spec/frontend/releases/detail/store/mutations_spec.js
new file mode 100644
index 00000000000..106a40c812e
--- /dev/null
+++ b/spec/frontend/releases/detail/store/mutations_spec.js
@@ -0,0 +1,119 @@
+/* eslint-disable jest/valid-describe */
+/*
+ * ESLint disable directive ↑ can be removed once
+ * https://github.com/jest-community/eslint-plugin-jest/issues/203
+ * is resolved
+ */
+
+import state from '~/releases/detail/store/state';
+import mutations from '~/releases/detail/store/mutations';
+import * as types from '~/releases/detail/store/mutation_types';
+import { release } from '../../mock_data';
+
+describe('Release detail mutations', () => {
+ let stateClone;
+ let releaseClone;
+
+ beforeEach(() => {
+ stateClone = state();
+ releaseClone = JSON.parse(JSON.stringify(release));
+ });
+
+ describe(types.SET_INITIAL_STATE, () => {
+ it('populates the state with initial values', () => {
+ const initialState = {
+ projectId: '18',
+ tagName: 'v1.3',
+ releasesPagePath: 'path/to/releases/page',
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
+ };
+
+ mutations[types.SET_INITIAL_STATE](stateClone, initialState);
+
+ expect(stateClone).toEqual(expect.objectContaining(initialState));
+ });
+ });
+
+ describe(types.REQUEST_RELEASE, () => {
+ it('set state.isFetchingRelease to true', () => {
+ mutations[types.REQUEST_RELEASE](stateClone);
+
+ expect(stateClone.isFetchingRelease).toEqual(true);
+ });
+ });
+
+ describe(types.RECEIVE_RELEASE_SUCCESS, () => {
+ it('handles a successful response from the server', () => {
+ mutations[types.RECEIVE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+ expect(stateClone.fetchError).toEqual(undefined);
+
+ expect(stateClone.isFetchingRelease).toEqual(false);
+
+ expect(stateClone.release).toEqual(releaseClone);
+ });
+ });
+
+ describe(types.RECEIVE_RELEASE_ERROR, () => {
+ it('handles an unsuccessful response from the server', () => {
+ const error = { message: 'An error occurred!' };
+ mutations[types.RECEIVE_RELEASE_ERROR](stateClone, error);
+
+ expect(stateClone.isFetchingRelease).toEqual(false);
+
+ expect(stateClone.release).toBeUndefined();
+
+ expect(stateClone.fetchError).toEqual(error);
+ });
+ });
+
+ describe(types.UPDATE_RELEASE_TITLE, () => {
+ it("updates the release's title", () => {
+ stateClone.release = releaseClone;
+ const newTitle = 'The new release title';
+ mutations[types.UPDATE_RELEASE_TITLE](stateClone, newTitle);
+
+ expect(stateClone.release.name).toEqual(newTitle);
+ });
+ });
+
+ describe(types.UPDATE_RELEASE_NOTES, () => {
+ it("updates the release's notes", () => {
+ stateClone.release = releaseClone;
+ const newNotes = 'The new release notes';
+ mutations[types.UPDATE_RELEASE_NOTES](stateClone, newNotes);
+
+ expect(stateClone.release.description).toEqual(newNotes);
+ });
+ });
+
+ describe(types.REQUEST_UPDATE_RELEASE, () => {
+ it('set state.isUpdatingRelease to true', () => {
+ mutations[types.REQUEST_UPDATE_RELEASE](stateClone);
+
+ expect(stateClone.isUpdatingRelease).toEqual(true);
+ });
+ });
+
+ describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
+ it('handles a successful response from the server', () => {
+ mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](stateClone, releaseClone);
+
+ expect(stateClone.updateError).toEqual(undefined);
+
+ expect(stateClone.isUpdatingRelease).toEqual(false);
+ });
+ });
+
+ describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
+ it('handles an unsuccessful response from the server', () => {
+ const error = { message: 'An error occurred!' };
+ mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](stateClone, error);
+
+ expect(stateClone.isUpdatingRelease).toEqual(false);
+
+ expect(stateClone.updateError).toEqual(error);
+ });
+ });
+});
diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb
new file mode 100644
index 00000000000..53953d72b06
--- /dev/null
+++ b/spec/helpers/environment_helper_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnvironmentHelper do
+ describe '#render_deployment_status' do
+ context 'when using a manual deployment' do
+ it 'renders a span tag' do
+ deploy = build(:deployment, deployable: nil, status: :success)
+ html = helper.render_deployment_status(deploy)
+
+ expect(html).to have_css('span.ci-status.ci-success')
+ end
+ end
+
+ context 'when using a deployment from a build' do
+ it 'renders a link tag' do
+ deploy = build(:deployment, status: :success)
+ html = helper.render_deployment_status(deploy)
+
+ expect(html).to have_css('a.ci-status.ci-success')
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 191df3cc709..cb6b158f01c 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -70,7 +70,7 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
-window.gon.ee = process.env.IS_GITLAB_EE;
+window.gon.ee = process.env.IS_EE;
gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
@@ -118,7 +118,7 @@ const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
const testContexts = [require.context('spec', true, /_spec$/)];
-if (process.env.IS_GITLAB_EE) {
+if (process.env.IS_EE) {
testContexts.push(require.context('ee_spec', true, /_spec$/));
}
@@ -207,7 +207,7 @@ if (process.env.BABEL_ENV === 'coverage') {
describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
- if (process.env.IS_GITLAB_EE) {
+ if (process.env.IS_EE) {
sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
}
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
new file mode 100644
index 00000000000..4b5c3f9489e
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Line do
+ let(:offset) { 0 }
+ let(:style) { Gitlab::Ci::Ansi2json::Style.new }
+
+ subject { described_class.new(offset: offset, style: style) }
+
+ describe '#<<' do
+ it 'appends new data to the current segment' do
+ expect { subject << 'test 1' }.to change { subject.current_segment.text }
+ expect(subject.current_segment.text).to eq('test 1')
+
+ expect { subject << ', test 2' }.to change { subject.current_segment.text }
+ expect(subject.current_segment.text).to eq('test 1, test 2')
+ end
+ end
+
+ describe '#style' do
+ context 'when style is passed to the initializer' do
+ let(:style) { double }
+
+ it 'returns the same style' do
+ expect(subject.style).to eq(style)
+ end
+ end
+
+ context 'when style is not passed to the initializer' do
+ it 'returns the default style' do
+ expect(subject.style.set?).to be_falsey
+ end
+ end
+ end
+
+ describe '#update_style' do
+ let(:expected_style) do
+ Gitlab::Ci::Ansi2json::Style.new(
+ fg: 'term-fg-l-yellow',
+ bg: 'term-bg-blue',
+ mask: 1)
+ end
+
+ it 'sets the style' do
+ subject.update_style(%w[1 33 44])
+
+ expect(subject.style).to eq(expected_style)
+ end
+ end
+
+ describe '#add_section' do
+ it 'appends a new section to the list' do
+ subject.add_section('section_1')
+ subject.add_section('section_2')
+
+ expect(subject.sections).to eq(%w[section_1 section_2])
+ end
+ end
+
+ describe '#set_as_section_header' do
+ it 'change the section_header to true' do
+ expect { subject.set_as_section_header }
+ .to change { subject.section_header }
+ .to be_truthy
+ end
+ end
+
+ describe '#set_section_duration' do
+ it 'sets and formats the section_duration' do
+ subject.set_section_duration(75)
+
+ expect(subject.section_duration).to eq('01:15')
+ end
+ end
+
+ describe '#flush_current_segment!' do
+ context 'when current segment is not empty' do
+ before do
+ subject << 'some data'
+ end
+
+ it 'adds the segment to the list' do
+ expect { subject.flush_current_segment! }.to change { subject.segments.count }.by(1)
+
+ expect(subject.segments.map { |s| s[:text] }).to eq(['some data'])
+ end
+
+ it 'updates the current segment pointer propagating the style' do
+ previous_segment = subject.current_segment
+
+ subject.flush_current_segment!
+
+ expect(subject.current_segment).not_to eq(previous_segment)
+ expect(subject.current_segment.style).to eq(previous_segment.style)
+ end
+ end
+
+ context 'when current segment is empty' do
+ it 'does not add any segments to the list' do
+ expect { subject.flush_current_segment! }.not_to change { subject.segments.count }
+ end
+
+ it 'does not change the current segment' do
+ expect { subject.flush_current_segment! }.not_to change { subject.current_segment }
+ end
+ end
+ end
+
+ describe '#to_h' do
+ before do
+ subject << 'some data'
+ subject.update_style(['1'])
+ end
+
+ context 'when sections are present' do
+ before do
+ subject.add_section('section_1')
+ subject.add_section('section_2')
+ end
+
+ context 'when section header is set' do
+ before do
+ subject.set_as_section_header
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_header: true
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+
+ context 'when section duration is set' do
+ before do
+ subject.set_section_duration(75)
+ end
+
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }],
+ section: 'section_2',
+ section_duration: '01:15'
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+ end
+
+ context 'when there are no sections' do
+ it 'serializes the attributes set' do
+ result = {
+ offset: 0,
+ content: [{ text: 'some data', style: 'term-bold' }]
+ }
+
+ expect(subject.to_h).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/parser_spec.rb b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
new file mode 100644
index 00000000000..e161e74c1ff
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/parser_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# The rest of the specs for this class are covered in style_spec.rb
+describe Gitlab::Ci::Ansi2json::Parser do
+ subject { described_class }
+
+ describe 'bold?' do
+ it 'returns true if style mask matches bold format' do
+ expect(subject.bold?(0x01)).to be_truthy
+ end
+
+ it 'returns false if style mask does not match bold format' do
+ expect(subject.bold?(0x02)).to be_falsey
+ end
+ end
+
+ describe 'matching_formats' do
+ it 'returns matching formats given a style mask' do
+ expect(subject.matching_formats(0x01)).to eq(%w[term-bold])
+ expect(subject.matching_formats(0x03)).to eq(%w[term-bold term-italic])
+ expect(subject.matching_formats(0x07)).to eq(%w[term-bold term-italic term-underline])
+ end
+
+ it 'returns an empty array if no formats match the style mask' do
+ expect(subject.matching_formats(0)).to eq([])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
new file mode 100644
index 00000000000..88a0ca35859
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json::Style do
+ describe '#set?' do
+ subject { described_class.new(params).set? }
+
+ context 'when fg color is set' do
+ let(:params) { { fg: 'term-fg-black' } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when bg color is set' do
+ let(:params) { { bg: 'term-bg-black' } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when mask is set' do
+ let(:params) { { mask: 0x01 } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'nothing is set' do
+ let(:params) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#reset!' do
+ let(:style) { described_class.new(fg: 'term-fg-black', bg: 'term-bg-yellow', mask: 0x01) }
+
+ it 'set the style params to default' do
+ style.reset!
+
+ expect(style.fg).to be_nil
+ expect(style.bg).to be_nil
+ expect(style.mask).to be_zero
+ end
+ end
+
+ describe 'update formats to mimic terminals' do
+ subject { described_class.new(params) }
+
+ context 'when fg color present' do
+ let(:params) { { fg: 'term-fg-black', mask: mask } }
+
+ context 'when mask is set to bold' do
+ let(:mask) { 0x01 }
+
+ it 'changes the fg color to a lighter version' do
+ expect(subject.fg).to eq('term-fg-l-black')
+ end
+ end
+
+ context 'when mask set to another format' do
+ let(:mask) { 0x02 }
+
+ it 'does not change the fg color' do
+ expect(subject.fg).to eq('term-fg-black')
+ end
+ end
+
+ context 'when mask is not set' do
+ let(:mask) { 0 }
+
+ it 'does not change the fg color' do
+ expect(subject.fg).to eq('term-fg-black')
+ end
+ end
+ end
+ end
+
+ describe '#update' do
+ where(:initial_state, :ansi_commands, :result, :description) do
+ [
+ # add format
+ [[], %w[0], '', 'does not set any style'],
+ [[], %w[1], 'term-bold', 'enables format bold'],
+ [[], %w[3], 'term-italic', 'enables format italic'],
+ [[], %w[4], 'term-underline', 'enables format underline'],
+ [[], %w[8], 'term-conceal', 'enables format conceal'],
+ [[], %w[9], 'term-cross', 'enables format cross'],
+ # remove format
+ [%w[1], %w[21], '', 'disables format bold'],
+ [%w[1 3], %w[21], 'term-italic', 'disables format bold and leaves italic'],
+ [%w[1], %w[22], '', 'disables format bold using command 22'],
+ [%w[1 3], %w[22], 'term-italic', 'disables format bold and leaves italic using command 22'],
+ [%w[3], %w[23], '', 'disables format italic'],
+ [%w[1 3], %w[23], 'term-bold', 'disables format italic and leaves bold'],
+ [%w[4], %w[24], '', 'disables format underline'],
+ [%w[1 4], %w[24], 'term-bold', 'disables format underline and leaves bold'],
+ [%w[8], %w[28], '', 'disables format conceal'],
+ [%w[1 8], %w[28], 'term-bold', 'disables format conceal and leaves bold'],
+ [%w[9], %w[29], '', 'disables format cross'],
+ [%w[1 9], %w[29], 'term-bold', 'disables format cross and leaves bold'],
+ # set fg color
+ [[], %w[30], 'term-fg-black', 'sets fg color black'],
+ [[], %w[31], 'term-fg-red', 'sets fg color red'],
+ [[], %w[32], 'term-fg-green', 'sets fg color green'],
+ [[], %w[33], 'term-fg-yellow', 'sets fg color yellow'],
+ [[], %w[34], 'term-fg-blue', 'sets fg color blue'],
+ [[], %w[35], 'term-fg-magenta', 'sets fg color magenta'],
+ [[], %w[36], 'term-fg-cyan', 'sets fg color cyan'],
+ [[], %w[37], 'term-fg-white', 'sets fg color white'],
+ # sets xterm fg color
+ [[], %w[38 5 1], 'xterm-fg-1', 'sets xterm fg color 1'],
+ [[], %w[38 5 2], 'xterm-fg-2', 'sets xterm fg color 2'],
+ [[], %w[38 1], 'term-bold', 'ignores 38 command if not followed by 5 and sets format bold'],
+ # set bg color
+ [[], %w[40], 'term-bg-black', 'sets bg color black'],
+ [[], %w[41], 'term-bg-red', 'sets bg color red'],
+ [[], %w[42], 'term-bg-green', 'sets bg color green'],
+ [[], %w[43], 'term-bg-yellow', 'sets bg color yellow'],
+ [[], %w[44], 'term-bg-blue', 'sets bg color blue'],
+ [[], %w[45], 'term-bg-magenta', 'sets bg color magenta'],
+ [[], %w[46], 'term-bg-cyan', 'sets bg color cyan'],
+ [[], %w[47], 'term-bg-white', 'sets bg color white'],
+ # set xterm bg color
+ [[], %w[48 5 1], 'xterm-bg-1', 'sets xterm bg color 1'],
+ [[], %w[48 5 2], 'xterm-bg-2', 'sets xterm bg color 2'],
+ [[], %w[48 1], 'term-bold', 'ignores 48 command if not followed by 5 and sets format bold'],
+ # set light fg color
+ [[], %w[90], 'term-fg-l-black', 'sets fg color light black'],
+ [[], %w[91], 'term-fg-l-red', 'sets fg color light red'],
+ [[], %w[92], 'term-fg-l-green', 'sets fg color light green'],
+ [[], %w[93], 'term-fg-l-yellow', 'sets fg color light yellow'],
+ [[], %w[94], 'term-fg-l-blue', 'sets fg color light blue'],
+ [[], %w[95], 'term-fg-l-magenta', 'sets fg color light magenta'],
+ [[], %w[96], 'term-fg-l-cyan', 'sets fg color light cyan'],
+ [[], %w[97], 'term-fg-l-white', 'sets fg color light white'],
+ # set light bg color
+ [[], %w[100], 'term-bg-l-black', 'sets bg color light black'],
+ [[], %w[101], 'term-bg-l-red', 'sets bg color light red'],
+ [[], %w[102], 'term-bg-l-green', 'sets bg color light green'],
+ [[], %w[103], 'term-bg-l-yellow', 'sets bg color light yellow'],
+ [[], %w[104], 'term-bg-l-blue', 'sets bg color light blue'],
+ [[], %w[105], 'term-bg-l-magenta', 'sets bg color light magenta'],
+ [[], %w[106], 'term-bg-l-cyan', 'sets bg color light cyan'],
+ [[], %w[107], 'term-bg-l-white', 'sets bg color light white'],
+ # reset
+ [%w[1], %w[0], '', 'resets style from format bold'],
+ [%w[1 3], %w[0], '', 'resets style from format bold and italic'],
+ [%w[1 3 term-fg-l-red term-bg-yellow], %w[0], '', 'resets all formats and colors'],
+ # misc
+ [[], %w[1 30 42 3], 'term-fg-l-black term-bg-green term-bold term-italic', 'adds fg color, bg color and formats from no style'],
+ [%w[3 31], %w[23 1 43], 'term-fg-l-red term-bg-yellow term-bold', 'replaces format italic with bold and adds a yellow background']
+ ]
+ end
+
+ with_them do
+ it 'change the style' do
+ style = described_class.new
+ style.update(initial_state)
+
+ style.update(ansi_commands)
+
+ expect(style.to_s).to eq(result)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
new file mode 100644
index 00000000000..3c6bc46436b
--- /dev/null
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -0,0 +1,544 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Ansi2json do
+ subject { described_class }
+
+ describe 'lines' do
+ it 'prints non-ansi as-is' do
+ expect(convert_json('Hello')).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] }
+ ])
+ end
+
+ it 'adds new line in a separate element' do
+ expect(convert_json("Hello\nworld")).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] },
+ { offset: 6, content: [{ text: 'world' }] }
+ ])
+ end
+
+ it 'recognizes color changing ANSI sequences' do
+ expect(convert_json("\e[31mHello\e[0m")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] }
+ ])
+ end
+
+ it 'recognizes color changing ANSI sequences across multiple lines' do
+ expect(convert_json("\e[31mHello\nWorld\e[0m")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] },
+ { offset: 11, content: [{ text: 'World', style: 'term-fg-red' }] }
+ ])
+ end
+
+ it 'recognizes background and foreground colors' do
+ expect(convert_json("\e[31;44mHello")).to eq([
+ { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red term-bg-blue' }] }
+ ])
+ end
+
+ it 'recognizes style changes within the same line' do
+ expect(convert_json("\e[31;44mHello\e[0m world")).to eq([
+ { offset: 0, content: [
+ { text: 'Hello', style: 'term-fg-red term-bg-blue' },
+ { text: ' world' }
+ ] }
+ ])
+ end
+
+ context 'with section markers' do
+ let(:section_name) { 'prepare-script' }
+ let(:section_duration) { 63.seconds }
+ let(:section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+ it 'marks the first line of the section as header' do
+ expect(convert_json("Hello#{section_start}world!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script',
+ section_header: true
+ }
+ ])
+ end
+
+ it 'does not marks the other lines of the section as header' do
+ expect(convert_json("outside section#{section_start}Hello\nworld!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'outside section' }]
+ },
+ {
+ offset: 15,
+ content: [{ text: 'Hello' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 65,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script'
+ }
+ ])
+ end
+
+ it 'marks the last line of the section as footer' do
+ expect(convert_json("#{section_start}Good\nmorning\nworld!#{section_end}")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Good' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 49,
+ content: [{ text: 'morning' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 57,
+ content: [{ text: 'world!' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 63,
+ content: [],
+ section_duration: '01:03',
+ section: 'prepare-script'
+ },
+ {
+ offset: 63,
+ content: []
+ }
+ ])
+ end
+
+ it 'marks the first line as header and footer if is the only line in the section' do
+ expect(convert_json("#{section_start}Hello world!#{section_end}")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello world!' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 56,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 56,
+ content: []
+ }
+ ])
+ end
+
+ it 'does not add sections attribute to lines after the section is closed' do
+ expect(convert_json("#{section_start}Hello#{section_end}world")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 49,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 49,
+ content: [{ text: 'world' }]
+ }
+ ])
+ end
+
+ it 'ignores section_end marker if no section_start exists' do
+ expect(convert_json("Hello #{section_end}world")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello world' }]
+ }
+ ])
+ end
+
+ context 'when section name contains .-_ and capital letters' do
+ let(:section_name) { 'a.Legit-SeCtIoN_namE' }
+
+ it 'sanitizes the section name' do
+ expect(convert_json("Hello#{section_start}world!")).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'world!' }],
+ section: 'a-legit-section-name',
+ section_header: true
+ }
+ ])
+ end
+ end
+
+ context 'when section name includes $' do
+ let(:section_name) { 'my_$ection' }
+
+ it 'ignores the section' do
+ expect(convert_json("#{section_start}hello")).to eq([
+ {
+ offset: 0,
+ content: [{ text: "#{section_start.gsub("\033[0K", '')}hello" }]
+ }
+ ])
+ end
+ end
+
+ context 'when section name includes <' do
+ let(:section_name) { '<a_tag>' }
+
+ it 'ignores the section' do
+ expect(convert_json("#{section_start}hello")).to eq([
+ {
+ offset: 0,
+ content: [{ text: "#{section_start.gsub("\033[0K", '').gsub('<', '&lt;')}hello" }]
+ }
+ ])
+ end
+ end
+
+ it 'prevents XSS injection' do
+ trace = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}"
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: "section_end:1:2&lt;script>alert('XSS Hack!');&lt;/script>" }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 95,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 95,
+ content: []
+ }
+ ])
+ end
+
+ context 'with nested section' do
+ let(:nested_section_name) { 'prepare-script-nested' }
+ let(:nested_section_duration) { 2.seconds }
+ let(:nested_section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:nested_section_end_time) { nested_section_start_time + nested_section_duration }
+ let(:nested_section_start) { "section_start:#{nested_section_start_time.to_i}:#{nested_section_name}\r\033[0K"}
+ let(:nested_section_end) { "section_end:#{nested_section_end_time.to_i}:#{nested_section_name}\r\033[0K"}
+
+ it 'adds multiple sections to the lines inside the nested section' do
+ trace = "Hello#{section_start}foo#{nested_section_start}bar#{nested_section_end}baz#{section_end}world"
+
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'foo' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 52,
+ content: [{ text: 'bar' }],
+ section: 'prepare-script-nested',
+ section_header: true
+ },
+ {
+ offset: 106,
+ content: [],
+ section: 'prepare-script-nested',
+ section_duration: '00:02'
+ },
+ {
+ offset: 106,
+ content: [{ text: 'baz' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 158,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 158,
+ content: [{ text: 'world' }]
+ }
+ ])
+ end
+
+ it 'adds multiple sections to the lines inside the nested section and closes all sections together' do
+ trace = "Hello#{section_start}\e[91mfoo\e[0m#{nested_section_start}bar#{nested_section_end}#{section_end}"
+
+ expect(convert_json(trace)).to eq([
+ {
+ offset: 0,
+ content: [{ text: 'Hello' }]
+ },
+ {
+ offset: 5,
+ content: [{ text: 'foo', style: 'term-fg-l-red' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 61,
+ content: [{ text: 'bar' }],
+ section: 'prepare-script-nested',
+ section_header: true
+ },
+ {
+ offset: 115,
+ content: [],
+ section: 'prepare-script-nested',
+ section_duration: '00:02'
+ },
+ {
+ offset: 115,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 164,
+ content: []
+ }
+ ])
+ end
+ end
+ end
+
+ describe 'incremental updates' do
+ let(:pass1_stream) { StringIO.new(pre_text) }
+ let(:pass2_stream) { StringIO.new(pre_text + text) }
+ let(:pass1) { subject.convert(pass1_stream) }
+ let(:pass2) { subject.convert(pass2_stream, pass1.state) }
+
+ context 'with split word' do
+ let(:pre_text) { "\e[1mHello " }
+ let(:text) { "World" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [{ text: 'Hello World', style: 'term-bold' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split word on second line' do
+ let(:pre_text) { "Good\nmorning " }
+ let(:text) { "World" }
+
+ let(:lines) do
+ [
+ { offset: 5, content: [{ text: 'morning World' }] }
+ ]
+ end
+
+ it 'returns all lines since last partially processed line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split sequence across multiple lines' do
+ let(:pre_text) { "\e[1mgood\nmorning\n" }
+ let(:text) { "\e[3mworld" }
+
+ let(:lines) do
+ [
+ { offset: 17, content: [{ text: 'world', style: 'term-bold term-italic' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split partial sequence' do
+ let(:pre_text) { "hello\e" }
+ let(:text) { "[1m world" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [
+ { text: 'hello' },
+ { text: ' world', style: 'term-bold' }
+ ] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split new line' do
+ let(:pre_text) { "hello\r" }
+ let(:text) { "\nworld" }
+
+ let(:lines) do
+ [
+ { offset: 0, content: [{ text: 'hello' }] },
+ { offset: 7, content: [{ text: 'world' }] }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split section' do
+ let(:section_name) { 'prepare-script' }
+ let(:section_duration) { 63.seconds }
+ let(:section_start_time) { Time.new(2019, 9, 17).utc }
+ let(:section_end_time) { section_start_time + section_duration }
+ let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
+ let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
+
+ context 'with split section body' do
+ let(:pre_text) { "#{section_start}this is a header\nand " }
+ let(:text) { "this\n is a body" }
+
+ let(:lines) do
+ [
+ {
+ offset: 61,
+ content: [{ text: 'and this' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 70,
+ content: [{ text: ' is a body' }],
+ section: 'prepare-script'
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+
+ context 'with split section where header is also split' do
+ let(:pre_text) { "#{section_start}this is " }
+ let(:text) { "a header\nand body" }
+
+ let(:lines) do
+ [
+ {
+ offset: 0,
+ content: [{ text: 'this is a header' }],
+ section: 'prepare-script',
+ section_header: true
+ },
+ {
+ offset: 61,
+ content: [{ text: 'and body' }],
+ section: 'prepare-script'
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_falsey
+ end
+ end
+
+ context 'with split section end' do
+ let(:pre_text) { "#{section_start}this is a header\nthe" }
+ let(:text) { " body\nthe end#{section_end}" }
+
+ let(:lines) do
+ [
+ {
+ offset: 61,
+ content: [{ text: 'the body' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 70,
+ content: [{ text: 'the end' }],
+ section: 'prepare-script'
+ },
+ {
+ offset: 77,
+ content: [],
+ section: 'prepare-script',
+ section_duration: '01:03'
+ },
+ {
+ offset: 77,
+ content: []
+ }
+ ]
+ end
+
+ it 'returns the full line' do
+ expect(pass2.lines).to eq(lines)
+ expect(pass2.append).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe 'trucates' do
+ let(:text) { "Hello World" }
+ let(:stream) { StringIO.new(text) }
+ let(:subject) { described_class.convert(stream) }
+
+ before do
+ stream.seek(3, IO::SEEK_SET)
+ end
+
+ it "returns truncated output" do
+ expect(subject.truncated).to be_truthy
+ end
+
+ it "does not append output" do
+ expect(subject.append).to be_falsey
+ end
+ end
+
+ def convert_json(data)
+ stream = StringIO.new(data)
+ subject.convert(stream).lines
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/position_collection_spec.rb b/spec/lib/gitlab/diff/position_collection_spec.rb
index de0e631ab03..f2a8312587c 100644
--- a/spec/lib/gitlab/diff/position_collection_spec.rb
+++ b/spec/lib/gitlab/diff/position_collection_spec.rb
@@ -35,14 +35,15 @@ describe Gitlab::Diff::PositionCollection do
let(:text_position) { build_text_position }
let(:folded_text_position) { build_text_position(old_line: 1, new_line: 1) }
let(:image_position) { build_image_position }
+ let(:invalid_position) { 'a position' }
let(:head_sha) { merge_request.diff_head_sha }
let(:collection) do
- described_class.new([text_position, folded_text_position, image_position], head_sha)
+ described_class.new([text_position, folded_text_position, image_position, invalid_position], head_sha)
end
describe '#to_a' do
- it 'returns all positions' do
+ it 'returns all positions that are Gitlab::Diff::Position' do
expect(collection.to_a).to eq([text_position, folded_text_position, image_position])
end
end
@@ -59,6 +60,14 @@ describe Gitlab::Diff::PositionCollection do
expect(collection.unfoldable).to be_empty
end
end
+
+ context 'when given head_sha is nil' do
+ let(:head_sha) { nil }
+
+ it 'returns unfoldable diff positions unfiltered by head_sha' do
+ expect(collection.unfoldable).to eq([folded_text_position])
+ end
+ end
end
describe '#concat' do
diff --git a/spec/lib/gitlab/health_checks/probes/collection_spec.rb b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
new file mode 100644
index 00000000000..33efc640257
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/probes/collection_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::Probes::Collection do
+ let(:readiness) { described_class.new(*checks) }
+
+ describe '#call' do
+ subject { readiness.execute }
+
+ context 'with all checks' do
+ let(:checks) do
+ [
+ Gitlab::HealthChecks::DbCheck,
+ Gitlab::HealthChecks::Redis::RedisCheck,
+ Gitlab::HealthChecks::Redis::CacheCheck,
+ Gitlab::HealthChecks::Redis::QueuesCheck,
+ Gitlab::HealthChecks::Redis::SharedStateCheck,
+ Gitlab::HealthChecks::GitalyCheck
+ ]
+ end
+
+ it 'responds with readiness checks data' do
+ expect(subject.http_status).to eq(200)
+
+ expect(subject.json[:status]).to eq('ok')
+ expect(subject.json['db_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['gitaly_check']).to contain_exactly(
+ status: 'ok', labels: { shard: 'default' })
+ end
+
+ context 'when Redis fails' do
+ before do
+ allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+ end
+
+ it 'responds with failure' do
+ expect(subject.http_status).to eq(503)
+
+ expect(subject.json[:status]).to eq('failed')
+ expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
+ expect(subject.json['redis_check']).to contain_exactly(
+ status: 'failed', message: 'check error')
+ end
+ end
+ end
+
+ context 'without checks' do
+ let(:checks) { [] }
+
+ it 'responds with success' do
+ expect(subject.http_status).to eq(200)
+
+ expect(subject.json).to eq(status: 'ok')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/probes/liveness_spec.rb b/spec/lib/gitlab/health_checks/probes/liveness_spec.rb
deleted file mode 100644
index 91066cb8ba0..00000000000
--- a/spec/lib/gitlab/health_checks/probes/liveness_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::Probes::Liveness do
- let(:liveness) { described_class.new }
-
- describe '#call' do
- subject { liveness.execute }
-
- it 'responds with liveness checks data' do
- expect(subject.http_status).to eq(200)
-
- expect(subject.json[:status]).to eq('ok')
- end
- end
-end
diff --git a/spec/lib/gitlab/health_checks/probes/readiness_spec.rb b/spec/lib/gitlab/health_checks/probes/readiness_spec.rb
deleted file mode 100644
index d88ffd984c2..00000000000
--- a/spec/lib/gitlab/health_checks/probes/readiness_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::Probes::Readiness do
- let(:readiness) { described_class.new }
-
- describe '#call' do
- subject { readiness.execute }
-
- it 'responds with readiness checks data' do
- expect(subject.http_status).to eq(200)
-
- expect(subject.json[:status]).to eq('ok')
- expect(subject.json['db_check']).to contain_exactly(status: 'ok')
- expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
- expect(subject.json['queues_check']).to contain_exactly(status: 'ok')
- expect(subject.json['shared_state_check']).to contain_exactly(status: 'ok')
- expect(subject.json['gitaly_check']).to contain_exactly(
- status: 'ok', labels: { shard: 'default' })
- end
-
- context 'when Redis fails' do
- before do
- allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
- Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
- end
-
- it 'responds with failure' do
- expect(subject.http_status).to eq(503)
-
- expect(subject.json[:status]).to eq('failed')
- expect(subject.json['cache_check']).to contain_exactly(status: 'ok')
- expect(subject.json['redis_check']).to contain_exactly(
- status: 'failed', message: 'check error')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index c1d171815ba..6bf837f1d3f 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -146,7 +146,7 @@ describe Gitlab do
describe '.ee?' do
before do
- stub_env('IS_GITLAB_EE', nil) # Make sure the ENV is clean
+ stub_env('FOSS_ONLY', nil) # Make sure the ENV is clean
described_class.instance_variable_set(:@is_ee, nil)
end
@@ -154,42 +154,66 @@ describe Gitlab do
described_class.instance_variable_set(:@is_ee, nil)
end
- it 'returns true when using Enterprise Edition' do
- root = Pathname.new('dummy')
- license_path = double(:path, exist?: true)
+ context 'for EE' do
+ before do
+ root = Pathname.new('dummy')
+ license_path = double(:path, exist?: true)
- allow(described_class)
- .to receive(:root)
- .and_return(root)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(root)
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ allow(root)
+ .to receive(:join)
+ .with('ee/app/models/license.rb')
+ .and_return(license_path)
+ end
- expect(described_class.ee?).to eq(true)
- end
+ context 'when using FOSS_ONLY=1' do
+ before do
+ stub_env('FOSS_ONLY', '1')
+ end
- it 'returns false when using Community Edition' do
- root = double(:path)
- license_path = double(:path, exists?: false)
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
+ end
- allow(described_class)
- .to receive(:root)
- .and_return(Pathname.new('dummy'))
+ context 'when using FOSS_ONLY=0' do
+ before do
+ stub_env('FOSS_ONLY', '0')
+ end
- allow(root)
- .to receive(:join)
- .with('ee/app/models/license.rb')
- .and_return(license_path)
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
- expect(described_class.ee?).to eq(false)
+ context 'when using default FOSS_ONLY' do
+ it 'returns to be EE' do
+ expect(described_class).to be_ee
+ end
+ end
end
- it 'returns true when the IS_GITLAB_EE variable is not empty' do
- stub_env('IS_GITLAB_EE', '1')
+ context 'for CE' do
+ before do
+ root = double(:path)
+ license_path = double(:path, exists?: false)
- expect(described_class.ee?).to eq(true)
+ allow(described_class)
+ .to receive(:root)
+ .and_return(Pathname.new('dummy'))
+
+ allow(root)
+ .to receive(:join)
+ .with('ee/app/models/license.rb')
+ .and_return(license_path)
+ end
+
+ it 'returns not to be EE' do
+ expect(described_class).not_to be_ee
+ end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 84c25b93fc6..702a6fab0e6 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -56,6 +56,10 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:protected_paths) }
it { is_expected.to allow_value([]).for(:protected_paths) }
+ it { is_expected.to allow_value(3).for(:push_event_hooks_limit) }
+ it { is_expected.not_to allow_value('three').for(:push_event_hooks_limit) }
+ it { is_expected.not_to allow_value(nil).for(:push_event_hooks_limit) }
+
context "when user accepted let's encrypt terms of service" do
before do
setting.update(lets_encrypt_terms_of_service_accepted: true)
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 51ed8e9421b..3a0b3c46ad0 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -348,4 +348,17 @@ describe Deployment do
expect(deployment.deployed_by).to eq(build_user)
end
end
+
+ describe '.find_successful_deployment!' do
+ it 'returns a successful deployment' do
+ deploy = create(:deployment, :success)
+
+ expect(described_class.find_successful_deployment!(deploy.iid)).to eq(deploy)
+ end
+
+ it 'raises when no deployment is found' do
+ expect { described_class.find_successful_deployment!(-1) }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 521c4704c87..786f3b832c4 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -882,4 +882,19 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '.find_or_create_by_name' do
+ it 'finds an existing environment if it exists' do
+ env = create(:environment)
+
+ expect(described_class.find_or_create_by_name(env.name)).to eq(env)
+ end
+
+ it 'creates an environment if it does not exist' do
+ env = project.environments.find_or_create_by_name('kittens')
+
+ expect(env).to be_an_instance_of(described_class)
+ expect(env).to be_persisted
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 6093464c949..e61a064e82c 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -40,14 +40,14 @@ describe ProjectPolicy do
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image
- create_environment create_deployment create_release update_release
+ create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 3dac7225b7a..ad7be531979 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Deployments do
@@ -96,4 +98,164 @@ describe API::Deployments do
end
end
end
+
+ describe 'POST /projects/:id/deployments' do
+ let!(:project) { create(:project, :repository) }
+ let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ context 'as a maintainer' do
+ it 'creates a new deployment' do
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(201)
+
+ expect(json_response['sha']).to eq(sha)
+ expect(json_response['ref']).to eq('master')
+ expect(json_response['environment']['name']).to eq('production')
+ end
+
+ it 'errors when creating a deployment with an invalid name' do
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'a' * 300,
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(500)
+ end
+ end
+
+ context 'as a developer' do
+ it 'creates a new deployment' do
+ developer = create(:user)
+
+ project.add_developer(developer)
+
+ post(
+ api("/projects/#{project.id}/deployments", developer),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(201)
+
+ expect(json_response['sha']).to eq(sha)
+ expect(json_response['ref']).to eq('master')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ post(
+ api( "/projects/#{project.id}/deployments", non_member),
+ params: {
+ environment: 'production',
+ sha: '123',
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/deployments/:deployment_id' do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, :failed, project: project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:deploy) do
+ create(
+ :deployment,
+ :failed,
+ project: project,
+ environment: environment,
+ deployable: nil
+ )
+ end
+
+ context 'as a maintainer' do
+ it 'returns a 403 when updating a deployment with a build' do
+ deploy.update(deployable: build)
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a deployment without an associated build' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('success')
+ end
+ end
+
+ context 'as a developer' do
+ let(:developer) { create(:user) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'returns a 403 when updating a deployment with a build' do
+ deploy.update(deployable: build)
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a deployment without an associated build' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", developer),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('success')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", non_member),
+ params: { status: 'success' }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 7e67ee28bef..eb55d747179 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -87,6 +87,15 @@ describe API::Members do
expect(json_response.first['username']).to eq(maintainer.username)
end
+ it 'finds members with the given user_ids' do
+ get api(members_url, developer), params: { user_ids: [maintainer.id, developer.id, stranger.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |u| u['id'] }).to contain_exactly(maintainer.id, developer.id)
+ end
+
it 'finds all members with no query specified' do
get api(members_url, developer), params: { query: '' }
@@ -155,10 +164,10 @@ describe API::Members do
end
end
- shared_examples 'GET /:source_type/:id/members/:user_id' do |source_type|
- context "with :source_type == #{source_type.pluralize}" do
+ shared_examples 'GET /:source_type/:id/members/(all/):user_id' do |source_type, all|
+ context "with :source_type == #{source_type.pluralize} and all == #{all}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", stranger) }
end
context 'when authenticated as a non-member' do
@@ -166,7 +175,7 @@ describe API::Members do
context "as a #{type}" do
it 'returns 200' do
user = public_send(type)
- get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+ get api("/#{source_type.pluralize}/#{source.id}/members/#{all ? 'all/' : ''}#{developer.id}", user)
expect(response).to have_gitlab_http_status(200)
# User attributes
@@ -434,12 +443,14 @@ describe API::Members do
end
end
- it_behaves_like 'GET /:source_type/:id/members/:user_id', 'project' do
- let(:source) { project }
- end
+ [false, true].each do |all|
+ it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'project', all do
+ let(:source) { all ? create(:project, :public, group: group) : project }
+ end
- it_behaves_like 'GET /:source_type/:id/members/:user_id', 'group' do
- let(:source) { group }
+ it_behaves_like 'GET /:source_type/:id/members/(all/):user_id', 'group', all do
+ let(:source) { all ? create(:group, parent: group) : group }
+ end
end
it_behaves_like 'POST /:source_type/:id/members', 'project' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index d98b9be726a..af1cf80e9d3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -72,7 +72,8 @@ describe API::Settings, 'Settings' do
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
- allow_local_requests_from_system_hooks: false
+ allow_local_requests_from_system_hooks: false,
+ push_event_hooks_limit: 2
}
expect(response).to have_gitlab_http_status(200)
@@ -102,6 +103,7 @@ describe API::Settings, 'Settings' do
expect(json_response['local_markdown_version']).to eq(3)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
expect(json_response['allow_local_requests_from_system_hooks']).to eq(false)
+ expect(json_response['push_event_hooks_limit']).to eq(2)
end
end
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/deployments/after_create_service_spec.rb
index 343dab8a974..b34483ea85b 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/deployments/after_create_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe UpdateDeploymentService do
+describe Deployments::AfterCreateService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } }
diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb
new file mode 100644
index 00000000000..e41c8259ea9
--- /dev/null
+++ b/spec/services/deployments/create_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::CreateService do
+ let(:environment) do
+ double(
+ :environment,
+ deployment_platform: double(:platform, cluster_id: 1),
+ project_id: 2,
+ id: 3
+ )
+ end
+
+ let(:user) { double(:user) }
+
+ describe '#execute' do
+ let(:service) { described_class.new(environment, user, {}) }
+
+ it 'does not run the AfterCreateService service if the deployment is not persisted' do
+ deploy = double(:deployment, persisted?: false)
+
+ expect(service)
+ .to receive(:create_deployment)
+ .and_return(deploy)
+
+ expect(Deployments::AfterCreateService)
+ .not_to receive(:new)
+
+ expect(service.execute).to eq(deploy)
+ end
+
+ it 'runs the AfterCreateService service if the deployment is persisted' do
+ deploy = double(:deployment, persisted?: true)
+ after_service = double(:after_create_service)
+
+ expect(service)
+ .to receive(:create_deployment)
+ .and_return(deploy)
+
+ expect(Deployments::AfterCreateService)
+ .to receive(:new)
+ .with(deploy)
+ .and_return(after_service)
+
+ expect(after_service)
+ .to receive(:execute)
+
+ expect(service.execute).to eq(deploy)
+ end
+ end
+
+ describe '#create_deployment' do
+ it 'creates a deployment' do
+ environment = build(:environment)
+ service = described_class.new(environment, user, {})
+
+ expect(environment.deployments)
+ .to receive(:create)
+ .with(an_instance_of(Hash))
+
+ service.create_deployment
+ end
+ end
+
+ describe '#deployment_attributes' do
+ it 'only includes attributes that we want to persist' do
+ service = described_class.new(
+ environment,
+ user,
+ ref: 'master',
+ tag: true,
+ sha: '123',
+ foo: 'bar',
+ on_stop: 'stop',
+ status: 'running'
+ )
+
+ expect(service.deployment_attributes).to eq(
+ cluster_id: 1,
+ project_id: 2,
+ environment_id: 3,
+ ref: 'master',
+ tag: true,
+ sha: '123',
+ user: user,
+ on_stop: 'stop',
+ status: 'running'
+ )
+ end
+ end
+end
diff --git a/spec/services/deployments/update_service_spec.rb b/spec/services/deployments/update_service_spec.rb
new file mode 100644
index 00000000000..a923099b82c
--- /dev/null
+++ b/spec/services/deployments/update_service_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::UpdateService do
+ let(:deploy) { create(:deployment, :running) }
+ let(:service) { described_class.new(deploy, status: 'success') }
+
+ describe '#execute' do
+ it 'updates the status of a deployment' do
+ expect(service.execute).to eq(true)
+ expect(deploy.status).to eq('success')
+ end
+ end
+end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index e71900e3c0d..90b3eb38469 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -8,7 +8,6 @@ describe Git::BaseHooksService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
-
let(:oldrev) { Gitlab::Git::BLANK_SHA }
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
@@ -26,7 +25,17 @@ describe Git::BaseHooksService do
let(:project) { create(:project, :repository) }
- subject { TestService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }) }
+ let(:params) do
+ {
+ change: {
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ }
+ }
+ end
+
+ subject { TestService.new(project, user, params) }
context '#execute_hooks' do
before do
@@ -83,5 +92,21 @@ describe Git::BaseHooksService do
end
end
end
+
+ context 'execute_project_hooks param set to false' do
+ before do
+ params[:execute_project_hooks] = false
+
+ allow(project).to receive(:has_active_hooks?).and_return(true)
+ allow(project).to receive(:has_active_services?).and_return(true)
+ end
+
+ it 'does not execute hooks and services' do
+ expect(project).not_to receive(:execute_hooks)
+ expect(project).not_to receive(:execute_services)
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/git/process_ref_changes_service_spec.rb b/spec/services/git/process_ref_changes_service_spec.rb
index 4d394a29867..eeb395f6c7b 100644
--- a/spec/services/git/process_ref_changes_service_spec.rb
+++ b/spec/services/git/process_ref_changes_service_spec.rb
@@ -28,12 +28,66 @@ describe Git::ProcessRefChangesService do
it "calls #{push_service_class}" do
expect(push_service_class)
.to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: true))
.exactly(changes.count).times
.and_return(service)
subject.execute
end
+ context 'changes exceed push_event_hooks_limit' do
+ def multiple_changes(change, count)
+ Array.new(count).map.with_index do |n, index|
+ { index: index, oldrev: change[:oldrev], newrev: change[:newrev], ref: "#{change[:ref]}#{n}" }
+ end
+ end
+
+ let(:push_event_hooks_limit) { 3 }
+
+ let(:changes) do
+ multiple_changes(
+ { oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/test" },
+ push_event_hooks_limit + 1
+ )
+ end
+
+ before do
+ stub_application_setting(push_event_hooks_limit: push_event_hooks_limit)
+ end
+
+ context 'git_push_execute_all_project_hooks is disabled' do
+ before do
+ stub_feature_flags(git_push_execute_all_project_hooks: false)
+ end
+
+ it "calls #{push_service_class} with execute_project_hooks set to false" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: false))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+
+ context 'git_push_execute_all_project_hooks is enabled' do
+ before do
+ stub_feature_flags(git_push_execute_all_project_hooks: true)
+ end
+
+ it "calls #{push_service_class} with execute_project_hooks set to true" do
+ expect(push_service_class)
+ .to receive(:new)
+ .with(project, project.owner, hash_including(execute_project_hooks: true))
+ .exactly(changes.count).times
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+
context 'pipeline creation' do
context 'with valid .gitlab-ci.yml' do
before do
diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb
index c97eeba87db..bbe793a81bc 100644
--- a/spec/support/features/rss_shared_examples.rb
+++ b/spec/support/features/rss_shared_examples.rb
@@ -8,7 +8,9 @@ end
shared_examples "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
- expect(page).to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+ expect(page)
+ .to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']")
+ .or have_css("a.js-rss-button[href*='feed_token=#{user.feed_token}']")
end
end
@@ -20,6 +22,8 @@ end
shared_examples "it has an RSS button without a feed token" do
it "shows the RSS button without a feed token" do
- expect(page).to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+ expect(page)
+ .to have_css("a:has(.fa-rss):not([href*='feed_token'])")
+ .or have_css("a.js-rss-button:not([href*='feed_token'])")
end
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 1aa40dcde3d..65398c13d90 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -38,14 +38,14 @@ RSpec.shared_context 'ProjectPolicy context' do
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
- create_environment create_deployment create_release update_release
+ create_environment create_deployment update_deployment create_release update_release
]
end
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb
index 1c68922b03d..7f2816d7535 100644
--- a/spec/workers/deployments/success_worker_spec.rb
+++ b/spec/workers/deployments/success_worker_spec.rb
@@ -8,8 +8,8 @@ describe Deployments::SuccessWorker do
context 'when successful deployment' do
let(:deployment) { create(:deployment, :success) }
- it 'executes UpdateDeploymentService' do
- expect(UpdateDeploymentService)
+ it 'executes Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService)
.to receive(:new).with(deployment).and_call_original
subject
@@ -19,8 +19,8 @@ describe Deployments::SuccessWorker do
context 'when canceled deployment' do
let(:deployment) { create(:deployment, :canceled) }
- it 'does not execute UpdateDeploymentService' do
- expect(UpdateDeploymentService).not_to receive(:new)
+ it 'does not execute Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
@@ -29,8 +29,8 @@ describe Deployments::SuccessWorker do
context 'when deploy record does not exist' do
let(:deployment) { nil }
- it 'does not execute UpdateDeploymentService' do
- expect(UpdateDeploymentService).not_to receive(:new)
+ it 'does not execute Deployments::AfterCreateService' do
+ expect(Deployments::AfterCreateService).not_to receive(:new)
subject
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 6983fea021c..34aaa9bb1e9 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -93,6 +93,8 @@ describe PostReceive do
end
context 'with changes' do
+ let(:push_service) { double(execute: true) }
+
before do
allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner)
allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT])