diff options
88 files changed, 2064 insertions, 1034 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bd29a266ccd..144063f208f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -66,6 +66,7 @@ stages: paths: - knapsack/ - rspec_flaky/ + - rspec_profiling/ .use-pg: &use-pg services: @@ -159,6 +160,7 @@ stages: - coverage/ - knapsack/ - rspec_flaky/ + - rspec_profiling/ - tmp/capybara/ reports: junit: junit_rspec.xml @@ -336,6 +338,7 @@ retrieve-tests-metadata: - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - mkdir -p rspec_flaky/ + - mkdir -p rspec_profiling/ - wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH - '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}' @@ -350,7 +353,7 @@ update-tests-metadata: - rspec_flaky/ policy: push script: - - retry gem install fog-aws mime-types activesupport --no-document + - retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json - FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH} @@ -358,6 +361,7 @@ update-tests-metadata: - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json + - scripts/insert-rspec-profiling-data flaky-examples-check: <<: *dedicated-runner @@ -484,6 +488,9 @@ setup-test-env: build-qa-image: <<: *review-docker + variables: + <<: *review-docker-variables + GIT_DEPTH: "20" stage: prepare script: - time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/ diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index eef1e877ff2..b4007c1ba7b 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -2,32 +2,10 @@ <!-- What problem do we solve? --> -### Target audience +### Intended users -<!--- For whom are we doing this? Include a [persona](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/) -listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A), -or define a specific company role, e.g. "Release Manager". - -Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply) - -- Parker, Product Manager, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#parker-product-manager -/label ~"Persona: Product Manager" - -- Delaney, Development Team Lead, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#delaney-development-team-lead -/label ~"Persona: Development Team Lead" - -- Sasha, Software Developer, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sasha-software-developer -/label ~"Persona: Software developer" - -- Devon, DevOps Engineer, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#devon-devops-engineer -/label ~"Persona: DevOps Engineer" - -- Sidney, Systems Administrator, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sidney-systems-administrator -/label ~"Persona: Systems Administrator" - -- Sam, Security Analyst, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sam-security-analyst -/label ~"Persona: Security Analyst" ---> +<!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later. +Personas can be found at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ --> ### Further details @@ -42,7 +42,7 @@ gem 'omniauth-shibboleth', '~> 1.3.0' gem 'omniauth-twitter', '~> 1.4' gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth-authentiq', '~> 0.3.3' -gem 'rack-oauth2', '~> 1.2.1' +gem 'rack-oauth2', '~> 1.9.3' gem 'jwt', '~> 2.1.0' # Spam and anti-bot protection diff --git a/Gemfile.lock b/Gemfile.lock index 634029e03de..1e59e0d56a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,7 +65,7 @@ GEM atomic (1.1.99) attr_encrypted (3.1.0) encryptor (~> 3.0.0) - attr_required (1.0.0) + attr_required (1.0.1) awesome_print (1.8.0) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) @@ -622,12 +622,12 @@ GEM rack-attack (4.4.1) rack rack-cors (1.0.2) - rack-oauth2 (1.2.3) - activesupport (>= 2.3) - attr_required (>= 0.0.5) - httpclient (>= 2.4) - multi_json (>= 1.3.6) - rack (>= 1.1) + rack-oauth2 (1.9.3) + activesupport + attr_required + httpclient + json-jwt (>= 1.9.0) + rack rack-protection (2.0.5) rack rack-proxy (0.6.0) @@ -1103,7 +1103,7 @@ DEPENDENCIES rack (= 2.0.6) rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) - rack-oauth2 (~> 1.2.1) + rack-oauth2 (~> 1.9.3) rack-proxy (~> 0.6.0) rails (= 5.0.7.1) rails-controller-testing diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 6ece8b92a30..be80661223c 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -1,14 +1,16 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; -import tablePagination from '../../vue_shared/components/table_pagination.vue'; -import environmentTable from '../components/environments_table.vue'; +import TablePagination from '~/vue_shared/components/table_pagination.vue'; +import containerMixin from 'ee_else_ce/environments/mixins/container_mixin'; +import EnvironmentTable from '../components/environments_table.vue'; export default { components: { - environmentTable, - tablePagination, + EnvironmentTable, + TablePagination, GlLoadingIcon, }, + mixins: [containerMixin], props: { isLoading: { type: Boolean, @@ -47,7 +49,15 @@ export default { <slot name="emptyState"></slot> <div v-if="!isLoading && environments.length > 0" class="table-holder"> - <environment-table :environments="environments" :can-read-environment="canReadEnvironment" /> + <environment-table + :environments="environments" + :can-read-environment="canReadEnvironment" + :canary-deployment-feature-id="canaryDeploymentFeatureId" + :show-canary-deployment-callout="showCanaryDeploymentCallout" + :user-callouts-path="userCalloutsPath" + :lock-promotion-svg-path="lockPromotionSvgPath" + :help-canary-deployments-path="helpCanaryDeploymentsPath" + /> <table-pagination v-if="pagination && pagination.totalPages > 1" diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 1e89dce69cb..a092bdfbc6c 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -4,6 +4,7 @@ import _ from 'underscore'; import { GlTooltipDirective } from '@gitlab/ui'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; import StopComponent from './environment_stop.vue'; @@ -34,10 +35,10 @@ export default { TerminalButtonComponent, MonitoringButtonComponent, }, - directives: { GlTooltip: GlTooltipDirective, }, + mixins: [environmentItemMixin], props: { model: { @@ -467,9 +468,18 @@ export default { <div v-if="!model.isFolder" class="table-mobile-header" role="rowheader"> {{ s__('Environments|Environment') }} </div> + + <span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard"> + <icon :name="deployIconName" /> + </span> + <span v-if="!model.isFolder" class="environment-name table-mobile-content"> <a class="qa-environment-link" :href="environmentPath"> {{ model.name }} </a> + <span v-if="isProtected" class="badge badge-success"> + {{ s__('Environments|protected') }} + </span> </span> + <span v-else class="folder-name" role="button" @click="onClickFolder"> <icon :name="folderIconName" class="folder-icon" /> diff --git a/app/assets/javascripts/environments/mixins/container_mixin.js b/app/assets/javascripts/environments/mixins/container_mixin.js new file mode 100644 index 00000000000..f2907c120f8 --- /dev/null +++ b/app/assets/javascripts/environments/mixins/container_mixin.js @@ -0,0 +1,29 @@ +export default { + props: { + canaryDeploymentFeatureId: { + type: String, + required: false, + default: null, + }, + showCanaryDeploymentCallout: { + type: Boolean, + required: false, + default: false, + }, + userCalloutsPath: { + type: String, + required: false, + default: null, + }, + lockPromotionSvgPath: { + type: String, + required: false, + default: null, + }, + helpCanaryDeploymentsPath: { + type: String, + required: false, + default: null, + }, + }, +}; diff --git a/app/assets/javascripts/environments/mixins/environment_item_mixin.js b/app/assets/javascripts/environments/mixins/environment_item_mixin.js new file mode 100644 index 00000000000..2dfed36ec99 --- /dev/null +++ b/app/assets/javascripts/environments/mixins/environment_item_mixin.js @@ -0,0 +1,13 @@ +export default { + computed: { + deployIconName() { + return ''; + }, + shouldRenderDeployBoard() { + return false; + }, + }, + methods: { + toggleDeployBoard() {}, + }, +}; diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js index 308ad9784e4..a4dad6f1615 100644 --- a/app/assets/javascripts/lib/utils/webpack.js +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -6,5 +6,5 @@ export function resetServiceWorkersPublicPath() { // see: https://webpack.js.org/guides/public-path/ const relativeRootPath = (gon && gon.relative_url_root) || ''; const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; - __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase + window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line } diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 68b753a4abf..5c59c0c32dd 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -87,27 +87,25 @@ export default { <span class="note-headline-light">@{{ author.username }}</span> </a> <span v-else>{{ __('A deleted user') }}</span> - <span class="note-headline-light"> - <span class="note-headline-meta"> - <span class="system-note-message"> <slot></slot> </span> - <template v-if="createdAt"> - <span class="system-note-separator"> - <template v-if="actionText">{{ actionText }}</template> - </span> - <a - :href="noteTimestampLink" - class="note-timestamp system-note-separator" - @click="updateTargetNoteHash" - > - <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> - </a> - </template> - <i - class="fa fa-spinner fa-spin editing-spinner" - aria-label="Comment is being updated" - aria-hidden="true" - ></i> - </span> + <span class="note-headline-light note-headline-meta"> + <span class="system-note-message"> <slot></slot> </span> + <template v-if="createdAt"> + <span class="system-note-separator"> + <template v-if="actionText">{{ actionText }}</template> + </span> + <a + :href="noteTimestampLink" + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash" + > + <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" /> + </a> + </template> + <i + class="fa fa-spinner fa-spin editing-spinner" + aria-label="Comment is being updated" + aria-hidden="true" + ></i> </span> </div> </template> diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index bd1e1895660..d67d88c4dba 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -19,6 +19,7 @@ export default class pipelinesMediator { this.poll = new Poll({ resource: this.service, method: 'getPipeline', + data: this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined, successCallback: this.successCallback.bind(this), errorCallback: this.errorCallback.bind(this), }); @@ -56,6 +57,19 @@ export default class pipelinesMediator { .getPipeline() .then(response => this.successCallback(response)) .catch(() => this.errorCallback()) - .finally(() => this.poll.restart()); + .finally(() => + this.poll.restart( + this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined, + ), + ); + } + + /** + * Backend expects paramets in the following format: `expanded[]=id&expanded[]=id` + */ + getExpandedParameters() { + return { + expanded: this.store.state.expandedPipelines, + }; } } diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js index a53a9cc8365..e44eb9cdfd1 100644 --- a/app/assets/javascripts/pipelines/services/pipeline_service.js +++ b/app/assets/javascripts/pipelines/services/pipeline_service.js @@ -5,8 +5,8 @@ export default class PipelineService { this.pipeline = endpoint; } - getPipeline() { - return axios.get(this.pipeline); + getPipeline(params) { + return axios.get(this.pipeline, { params }); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 126b00af552..44556060c65 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -1019,3 +1019,13 @@ padding-left: 50px; padding-bottom: $gl-padding; } + +.mr-compare { + .diff-file .file-title-flex-parent { + top: $header-height + 51px; + + .with-performance-bar & { + top: $performance-bar-height + $header-height + 51px; + } + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 72f48e98c24..faf85e151e3 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -283,8 +283,6 @@ $note-form-margin-left: 72px; } .system-note-message { - display: inline; - &::first-letter { text-transform: lowercase; } @@ -607,12 +605,6 @@ $note-form-margin-left: 72px; } .note-headline-meta { - display: inline-block; - - .system-note-message { - white-space: normal; - } - .system-note-separator { color: $gl-text-color-disabled; } diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index f476f428fdb..f378f7ac79a 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -17,6 +17,16 @@ module Groups redirect_to group_settings_ci_cd_path end + def update_auto_devops + if auto_devops_service.execute + flash[:notice] = s_('GroupSettings|Auto DevOps pipeline was updated for the group') + else + flash[:alert] = s_("GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}." % { error_messages: group.errors.full_messages }) + end + + redirect_to group_settings_ci_cd_path + end + private def define_ci_variables @@ -29,6 +39,14 @@ module Groups def authorize_admin_group! return render_404 unless can?(current_user, :admin_group, group) end + + def auto_devops_params + params.require(:group).permit(:auto_devops_enabled) + end + + def auto_devops_service + Groups::AutoDevopsService.new(group, current_user, auto_devops_params) + end end end end diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 67e7e475920..0f0d5350df6 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -9,4 +9,17 @@ module AutoDevopsHelper !project.repository.gitlab_ci_yml && !project.ci_service end + + def badge_for_auto_devops_scope(auto_devops_receiver) + return unless auto_devops_receiver.auto_devops_enabled? + + case auto_devops_receiver.first_auto_devops_config[:scope] + when :project + nil + when :group + s_('CICD|group enabled') + when :instance + s_('CICD|instance enabled') + end + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index a5c479bdc0c..dea34e812ca 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,6 +11,7 @@ class Namespace < ApplicationRecord include IgnorableColumn include FeatureGate include FromUnion + include Gitlab::Utils::StrongMemoize ignore_column :deleted_at @@ -267,6 +268,22 @@ class Namespace < ApplicationRecord owner.refresh_authorized_projects end + def auto_devops_enabled? + first_auto_devops_config[:status] + end + + def first_auto_devops_config + return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil? + + strong_memoize(:first_auto_devops_config) do + if has_parent? + parent.first_auto_devops_config + else + { scope: :instance, status: Gitlab::CurrentSettings.auto_devops_enabled? } + end + end + end + private def path_or_parent_changed? diff --git a/app/models/project.rb b/app/models/project.rb index 4cc13f372c1..aba63032cdf 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -631,12 +631,21 @@ class Project < ActiveRecord::Base end def has_auto_devops_implicitly_enabled? - auto_devops&.enabled.nil? && - (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) + auto_devops_config = first_auto_devops_config + + auto_devops_config[:scope] != :project && auto_devops_config[:status] end def has_auto_devops_implicitly_disabled? - auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) + auto_devops_config = first_auto_devops_config + + auto_devops_config[:scope] != :project && !auto_devops_config[:status] + end + + def first_auto_devops_config + return namespace.first_auto_devops_config if auto_devops&.enabled.nil? + + { scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) } end def daily_statistics_enabled? diff --git a/app/services/groups/auto_devops_service.rb b/app/services/groups/auto_devops_service.rb new file mode 100644 index 00000000000..1925e0cc0ea --- /dev/null +++ b/app/services/groups/auto_devops_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Groups + class AutoDevopsService < Groups::BaseService + def execute + raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_group, group) + + group.update(auto_devops_enabled: auto_devops_enabled) + end + + private + + def auto_devops_enabled + params[:auto_devops_enabled] + end + end +end diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml new file mode 100644 index 00000000000..e7efc0237c8 --- /dev/null +++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml @@ -0,0 +1,15 @@ += form_for group, url: update_auto_devops_group_settings_ci_cd_path(group), method: :patch do |f| + = form_errors(group) + %fieldset + .form-group + .card.auto-devops-card + .card-body + .form-check + = f.check_box :auto_devops_enabled, class: 'form-check-input', checked: group.auto_devops_enabled? + = f.label :auto_devops_enabled, class: 'form-check-label' do + %strong= s_('GroupSettings|Default to Auto DevOps pipeline for all projects within this group') + %span.badge.badge-info#auto-devops-badge= badge_for_auto_devops_scope(group) + .form-text.text-muted + = s_('GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') + = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' + = f.submit _('Save changes'), class: 'btn btn-success prepend-top-15' diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index d9332e36ef5..d0f5cd94002 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -19,3 +19,17 @@ = _('Register and see your runners for this group.') .settings-content = render 'groups/runners/index' + +%section.settings#auto-devops-settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Auto DevOps') + %button.btn.btn-default.js-settings-toggle{ type: "button" } + = expanded ? _('Collapse') : _('Expand') + %p + - auto_devops_url = help_page_path('topics/autodevops/index') + - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } + = s_('GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe } + + .settings-content + = render 'groups/settings/ci_cd/auto_devops_form', group: @group diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 70011d58c8a..92e34b3ceda 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -3,7 +3,7 @@ - if @merge_request.closed_without_fork? .alert.alert-danger - %p The source project of this merge request has been removed. + The source project of this merge request has been removed. .detail-page-header .detail-page-header-body diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 8c4d1c32ebe..fac68a36e79 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -8,15 +8,15 @@ .card.auto-devops-card .card-body .form-check - = form.check_box :enabled, class: 'form-check-input js-toggle-extra-settings', checked: @project.auto_devops_enabled? + = form.check_box :enabled, class: 'form-check-input js-toggle-extra-settings', checked: auto_devops_enabled = form.label :enabled, class: 'form-check-label' do %strong= s_('CICD|Default to Auto DevOps pipeline') - - if @project.has_auto_devops_implicitly_enabled? - %span.badge.badge-info.js-instance-default-badge= s_('CICD|instance enabled') + - if auto_devops_enabled + %span.badge.badge-info.js-instance-default-badge= badge_for_auto_devops_scope(@project) .form-text.text-muted = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' - .card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' } + .card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' } %p.settings-message.text-center - kubernetes_cluster_link = help_page_path('user/project/clusters/index') - kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link } diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 6966bf96724..548b7c06867 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -26,7 +26,7 @@ = s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.') = link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md') .settings-content - = render 'autodevops_form' + = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled? = render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 41d6ae79c81..6fec435cc87 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -36,14 +36,13 @@ = user_status(note.author) %span.note-headline-light = note.author.to_reference - %span.note-headline-light - %span.note-headline-meta - - if note.system - %span.system-note-message - = markdown_field(note, :note) - %span.system-note-separator - · - %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') + %span.note-headline-light.note-headline-meta + - if note.system + %span.system-note-message + = markdown_field(note, :note) + %span.system-note-separator + · + %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? .note-actions - if note.for_personal_snippet? diff --git a/changelogs/unreleased/10029-env-item.yml b/changelogs/unreleased/10029-env-item.yml new file mode 100644 index 00000000000..f4e742d3e17 --- /dev/null +++ b/changelogs/unreleased/10029-env-item.yml @@ -0,0 +1,5 @@ +--- +title: Removes EE differences for environment_item.vue +merge_request: +author: +type: other diff --git a/changelogs/unreleased/25942-remove-fake-repository-path-response.yml b/changelogs/unreleased/25942-remove-fake-repository-path-response.yml new file mode 100644 index 00000000000..e1da28ab03c --- /dev/null +++ b/changelogs/unreleased/25942-remove-fake-repository-path-response.yml @@ -0,0 +1,5 @@ +--- +title: Remove fake repository_path response +merge_request: 25942 +author: Fabio Papa +type: other diff --git a/changelogs/unreleased/52447-auto-devops-at-group-level.yml b/changelogs/unreleased/52447-auto-devops-at-group-level.yml new file mode 100644 index 00000000000..0a21c6a2b7b --- /dev/null +++ b/changelogs/unreleased/52447-auto-devops-at-group-level.yml @@ -0,0 +1,5 @@ +--- +title: Enable/disable Auto DevOps at the Group level +merge_request: 25533 +author: +type: added diff --git a/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml b/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml new file mode 100644 index 00000000000..e45db8eafc3 --- /dev/null +++ b/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml @@ -0,0 +1,5 @@ +--- +title: Use curl silent/show-error options on Auto DevOps +merge_request: 25954 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml b/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml new file mode 100644 index 00000000000..ebfb7aeaa1f --- /dev/null +++ b/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Keep inline as much as possible in system notes on issuable +merge_request: 25968 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml b/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml new file mode 100644 index 00000000000..e30f48ed1a8 --- /dev/null +++ b/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml @@ -0,0 +1,5 @@ +--- +title: Fix UI for closed MR when source project is removed +merge_request: 25967 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/fix-ide-web-worker-relative-url.yml b/changelogs/unreleased/fix-ide-web-worker-relative-url.yml new file mode 100644 index 00000000000..2accad68c4e --- /dev/null +++ b/changelogs/unreleased/fix-ide-web-worker-relative-url.yml @@ -0,0 +1,5 @@ +--- +title: Fixed Web IDE web workers not working with relative URLs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml b/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml new file mode 100644 index 00000000000..dadbd5c940f --- /dev/null +++ b/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml @@ -0,0 +1,5 @@ +--- +title: Fixed sticky headers in merge request creation diffs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/update-rack-oauth2.yml b/changelogs/unreleased/update-rack-oauth2.yml new file mode 100644 index 00000000000..dc2e7017695 --- /dev/null +++ b/changelogs/unreleased/update-rack-oauth2.yml @@ -0,0 +1,5 @@ +--- +title: Update rack-oauth2 1.2.1 -> 1.9.3 +merge_request: 17868 +author: +type: other diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index 2de310753a9..715e17057e0 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -1,7 +1,28 @@ +# frozen_string_literal: true + +return unless Rails.env.test? + module RspecProfilingExt - module PSQL - def establish_connection - ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL']) + module Collectors + class CSVWithTimestamps < ::RspecProfiling::Collectors::CSV + TIMESTAMP_FIELDS = %w(created_at updated_at).freeze + HEADERS = (::RspecProfiling::Collectors::CSV::HEADERS + TIMESTAMP_FIELDS).freeze + + def insert(attributes) + output << HEADERS.map do |field| + if TIMESTAMP_FIELDS.include?(field) + Time.now + else + attributes.fetch(field.to_sym) + end + end + end + + private + + def output + @output ||= ::CSV.open(path, "w").tap { |csv| csv << HEADERS } + end end end @@ -10,9 +31,13 @@ module RspecProfilingExt if ENV['CI_COMMIT_REF_NAME'] "#{defined?(Gitlab::License) ? 'ee' : 'ce'}:#{ENV['CI_COMMIT_REF_NAME']}" else - super + super&.chomp end end + + def sha + super&.chomp + end end module Run @@ -30,16 +55,11 @@ module RspecProfilingExt end end -if Rails.env.test? - RspecProfiling.configure do |config| - if ENV['RSPEC_PROFILING_POSTGRES_URL'].present? - RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL) - config.collector = RspecProfiling::Collectors::PSQL - end - - if ENV.key?('CI') - RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git) - RspecProfiling::Run.prepend(RspecProfilingExt::Run) - end +RspecProfiling.configure do |config| + if ENV.key?('CI') || ENV.key?('RSPEC_PROFILING') + RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git) + RspecProfiling::Run.prepend(RspecProfilingExt::Run) + config.collector = RspecProfilingExt::Collectors::CSVWithTimestamps + config.csv_path = -> { "rspec_profiling/#{Time.now.to_i}-#{SecureRandom.hex(8)}-rspec-data.csv" } end end diff --git a/config/routes/group.rb b/config/routes/group.rb index b3015529c6e..f42c1ee6e7d 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -31,6 +31,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do namespace :settings do resource :ci_cd, only: [:show], controller: 'ci_cd' do put :reset_registration_token + patch :update_auto_devops end end diff --git a/db/fixtures/development/03_settings.rb b/db/fixtures/development/02_settings.rb index 3a4a5d436bf..3a4a5d436bf 100644 --- a/db/fixtures/development/03_settings.rb +++ b/db/fixtures/development/02_settings.rb diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/03_project.rb index 9a5f7cf8175..46018cf68aa 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/03_project.rb @@ -60,7 +60,7 @@ Sidekiq::Testing.inline! do path: group_path ) group.description = FFaker::Lorem.sentence - group.save + group.save! group.add_owner(User.first) end diff --git a/db/fixtures/development/04_labels.rb b/db/fixtures/development/04_labels.rb new file mode 100644 index 00000000000..b9ae4098d76 --- /dev/null +++ b/db/fixtures/development/04_labels.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'digest/md5' + +class Gitlab::Seeder::GroupLabels + def initialize(group, label_per_group: 10) + @group = group + @label_per_group = label_per_group + end + + def seed! + @label_per_group.times do + label_title = FFaker::Product.brand + Labels::CreateService + .new(title: label_title, color: "##{Digest::MD5.hexdigest(label_title)[0..5]}") + .execute(group: @group) + print '.' + end + end +end + +class Gitlab::Seeder::ProjectLabels + def initialize(project, label_per_project: 5) + @project = project + @label_per_project = label_per_project + end + + def seed! + @label_per_project.times do + label_title = FFaker::Vehicle.model + Labels::CreateService + .new(title: label_title, color: "##{Digest::MD5.hexdigest(label_title)[0..5]}") + .execute(project: @project) + print '.' + end + end +end + +Gitlab::Seeder.quiet do + puts "\nGenerating group labels" + Group.all.find_each do |group| + Gitlab::Seeder::GroupLabels.new(group).seed! + end + + puts "\nGenerating project labels" + Project.all.find_each do |project| + Gitlab::Seeder::ProjectLabels.new(project).seed! + end +end diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb index 16243b72f9a..926401d8b9e 100644 --- a/db/fixtures/development/09_issues.rb +++ b/db/fixtures/development/09_issues.rb @@ -3,13 +3,17 @@ require './spec/support/sidekiq' Gitlab::Seeder.quiet do Project.all.each do |project| 10.times do + label_ids = project.labels.pluck(:id).sample(3) + label_ids += project.group.labels.sample(3) if project.group + issue_params = { title: FFaker::Lorem.sentence(6), description: FFaker::Lorem.sentence, state: ['opened', 'closed'].sample, milestone: project.milestones.sample, assignees: [project.team.users.sample], - created_at: rand(12).months.ago + created_at: rand(12).months.ago, + label_ids: label_ids } Issues::CreateService.new(project, project.team.users.sample, issue_params).execute diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 2051bcff8f0..1952f84ed62 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -12,13 +12,17 @@ Gitlab::Seeder.quiet do source_branch = branches.pop target_branch = branches.pop + label_ids = project.labels.pluck(:id).sample(3) + label_ids += project.group.labels.sample(3) if project.group + params = { source_branch: source_branch, target_branch: target_branch, title: FFaker::Lorem.sentence(6), description: FFaker::Lorem.sentences(3).join(" "), milestone: project.milestones.sample, - assignee: project.team.users.sample + assignee: project.team.users.sample, + label_ids: label_ids } # Only create MRs with users that are allowed to create MRs diff --git a/db/fixtures/development/22_labeled_issues_seed.rb b/db/fixtures/development/22_labeled_issues_seed.rb deleted file mode 100644 index 3730e9c7958..00000000000 --- a/db/fixtures/development/22_labeled_issues_seed.rb +++ /dev/null @@ -1,103 +0,0 @@ -# Creates a project with labeled issues for an user. -# Run this single seed file using: rake db:seed_fu FILTER=labeled USER_ID=74. -# If an USER_ID is not provided it will use the last created user. -require './spec/support/sidekiq' - -class Gitlab::Seeder::LabeledIssues - include ::Gitlab::Utils - - def initialize(user) - @user = user - end - - def seed! - Sidekiq::Testing.inline! do - group = create_group - - create_projects(group) - create_labels(group) - create_issues(group) - end - - print '.' - end - - private - - def create_group - group_name = "group_of_#{@user.username}_#{SecureRandom.hex(4)}" - - group_params = { - name: group_name, - path: group_name, - description: FFaker::Lorem.sentence - } - - Groups::CreateService.new(@user, group_params).execute - end - - def create_projects(group) - 5.times do - project_name = "project_#{SecureRandom.hex(6)}" - - params = { - namespace_id: group.id, - name: project_name, - description: FFaker::Lorem.sentence, - visibility_level: Gitlab::VisibilityLevel.values.sample - } - - Projects::CreateService.new(@user, params).execute - end - end - - def create_labels(group) - group.projects.each do |project| - 5.times do - label_title = FFaker::Vehicle.model - Labels::CreateService.new(title: label_title, color: "#69D100").execute(project: project) - end - end - - 10.times do - label_title = FFaker::Product.brand - Labels::CreateService.new(title: label_title).execute(group: group) - end - end - - def create_issues(group) - # Get only group labels - group_labels = - LabelsFinder.new(@user, group_id: group.id).execute.where.not(group_id: nil) - - group.projects.each do |project| - label_ids = project.labels.pluck(:id).sample(5) - label_ids.push(*group.labels.sample(4)) - - 20.times do - issue_params = { - title: FFaker::Lorem.sentence(6), - description: FFaker::Lorem.sentence, - state: 'opened', - label_ids: label_ids - - } - - Issues::CreateService.new(project, @user, issue_params).execute if project.project_feature.present? - end - end - end -end - -Gitlab::Seeder.quiet do - user_id = ENV['USER_ID'] - - user = - if user_id.present? - User.find(user_id) - else - User.last - end - - Gitlab::Seeder::LabeledIssues.new(user).seed! -end diff --git a/db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb b/db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb new file mode 100644 index 00000000000..93e7a84fb02 --- /dev/null +++ b/db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddAutoDevOpsEnabledToNamespaces < ActiveRecord::Migration[5.0] + DOWNTIME = false + + def change + add_column :namespaces, :auto_devops_enabled, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 59a76e21a5f..dda0445e3f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1377,6 +1377,7 @@ ActiveRecord::Schema.define(version: 20190301182457) do t.integer "cached_markdown_version" t.string "runners_token" t.string "runners_token_encrypted" + t.boolean "auto_devops_enabled" t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree t.index ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} diff --git a/doc/user/instance_statistics/convdev.md b/doc/user/instance_statistics/convdev.md index 247be1fb392..2c9e0ecbf65 100644 --- a/doc/user/instance_statistics/convdev.md +++ b/doc/user/instance_statistics/convdev.md @@ -2,7 +2,7 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30469) in GitLab 9.3. -NOTE: **NOTE** +NOTE: **Note:** Your GitLab instance's [usage ping](../admin_area/settings/usage_statistics.md#usage-ping-core-only) must be activated in order to use this feature. The Conversational Development Index (ConvDev Index) gives you an overview of your entire diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 91eb6a23701..8afe6dda414 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -7,9 +7,7 @@ module API before { authenticate! } - NOTEABLE_TYPES = [Issue, Snippet, MergeRequest, Commit].freeze - - NOTEABLE_TYPES.each do |noteable_type| + Helpers::DiscussionsHelpers.noteable_types.each do |noteable_type| parent_type = noteable_type.parent_class.to_s.underscore noteables_str = noteable_type.to_s.underscore.pluralize noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 64958ff982a..cb0d6d96f29 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -58,6 +58,22 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + def create_group + # This is a separate method so that EE can extend its behaviour, without + # having to modify this code directly. + ::Groups::CreateService + .new(current_user, declared_params(include_missing: false)) + .execute + end + + def update_group(group) + # This is a separate method so that EE can extend its behaviour, without + # having to modify this code directly. + ::Groups::UpdateService + .new(group, current_user, declared_params(include_missing: false)) + .execute + end + def find_group_projects(params) group = find_group!(params[:id]) options = { @@ -127,7 +143,7 @@ module API authorize! :create_group end - group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute + group = create_group if group.persisted? present group, with: Entities::GroupDetail, current_user: current_user @@ -153,7 +169,7 @@ module API group = find_group!(params[:id]) authorize! :admin_group, group - if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute + if update_group(group) present group, with: Entities::GroupDetail, current_user: current_user else render_validation_error!(group) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 825fab62034..b8bd180bdc1 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -6,6 +6,7 @@ module API include Helpers::Pagination SUDO_HEADER = "HTTP_SUDO".freeze + GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret".freeze SUDO_PARAM = :sudo API_USER_ENV = 'gitlab.api.user'.freeze @@ -212,10 +213,12 @@ module API end def authenticate_by_gitlab_shell_token! - input = params['secret_token'].try(:chomp) - unless Devise.secure_compare(secret_token, input) - unauthorized! - end + input = params['secret_token'] + input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER) + + input&.chomp! + + unauthorized! unless Devise.secure_compare(secret_token, input) end def authenticated_with_full_private_access! diff --git a/lib/api/helpers/discussions_helpers.rb b/lib/api/helpers/discussions_helpers.rb new file mode 100644 index 00000000000..94a5bf75c39 --- /dev/null +++ b/lib/api/helpers/discussions_helpers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Helpers + module DiscussionsHelpers + def self.noteable_types + # This is a method instead of a constant, allowing EE to more easily + # extend it. + [Issue, Snippet, MergeRequest, Commit] + end + end + end +end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 795dca5cf03..a068de4361c 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -3,6 +3,12 @@ module API module Helpers module NotesHelpers + def self.noteable_types + # This is a method instead of a constant, allowing EE to more easily + # extend it. + [Issue, MergeRequest, Snippet] + end + def update_note(noteable, note_id) note = noteable.notes.find(params[:note_id]) diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb new file mode 100644 index 00000000000..23574deb59b --- /dev/null +++ b/lib/api/helpers/resource_label_events_helpers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Helpers + module ResourceLabelEventsHelpers + def self.eventable_types + # This is a method instead of a constant, allowing EE to more easily + # extend it. + [Issue, MergeRequest] + end + end + end +end diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb new file mode 100644 index 00000000000..47fb5a36327 --- /dev/null +++ b/lib/api/helpers/search_helpers.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Helpers + module SearchHelpers + def self.global_search_scopes + # This is a separate method so that EE can redefine it. + %w(projects issues merge_requests milestones snippet_titles snippet_blobs) + end + + def self.group_search_scopes + # This is a separate method so that EE can redefine it. + %w(projects issues merge_requests milestones) + end + + def self.project_search_scopes + # This is a separate method so that EE can redefine it. + %w(issues merge_requests milestones notes wiki_blobs commits blobs) + end + end + end +end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb new file mode 100644 index 00000000000..8582c45798f --- /dev/null +++ b/lib/api/helpers/services_helpers.rb @@ -0,0 +1,721 @@ +# frozen_string_literal: true + +module API + module Helpers + # Helpers module for API::Services + # + # The data structures inside this model are returned using class methods, + # allowing EE to extend them where necessary. + module ServicesHelpers + def self.chat_notification_settings + [ + { + required: true, + name: :webhook, + type: String, + desc: 'The chat webhook' + }, + { + required: false, + name: :username, + type: String, + desc: 'The chat username' + }, + { + required: false, + name: :channel, + type: String, + desc: 'The default chat channel' + } + ].freeze + end + + def self.chat_notification_flags + [ + { + required: false, + name: :notify_only_broken_pipelines, + type: Boolean, + desc: 'Send notifications for broken pipelines' + }, + { + required: false, + name: :notify_only_default_branch, + type: Boolean, + desc: 'Send notifications only for the default branch' + } + ].freeze + end + + def self.chat_notification_channels + [ + { + required: false, + name: :push_channel, + type: String, + desc: 'The name of the channel to receive push_events notifications' + }, + { + required: false, + name: :issue_channel, + type: String, + desc: 'The name of the channel to receive issues_events notifications' + }, + { + required: false, + name: :confidential_issue_channel, + type: String, + desc: 'The name of the channel to receive confidential_issues_events notifications' + }, + { + required: false, + name: :merge_request_channel, + type: String, + desc: 'The name of the channel to receive merge_requests_events notifications' + }, + { + required: false, + name: :note_channel, + type: String, + desc: 'The name of the channel to receive note_events notifications' + }, + { + required: false, + name: :tag_push_channel, + type: String, + desc: 'The name of the channel to receive tag_push_events notifications' + }, + { + required: false, + name: :pipeline_channel, + type: String, + desc: 'The name of the channel to receive pipeline_events notifications' + }, + { + required: false, + name: :wiki_page_channel, + type: String, + desc: 'The name of the channel to receive wiki_page_events notifications' + } + ].freeze + end + + def self.chat_notification_events + [ + { + required: false, + name: :push_events, + type: Boolean, + desc: 'Enable notifications for push_events' + }, + { + required: false, + name: :issues_events, + type: Boolean, + desc: 'Enable notifications for issues_events' + }, + { + required: false, + name: :confidential_issues_events, + type: Boolean, + desc: 'Enable notifications for confidential_issues_events' + }, + { + required: false, + name: :merge_requests_events, + type: Boolean, + desc: 'Enable notifications for merge_requests_events' + }, + { + required: false, + name: :note_events, + type: Boolean, + desc: 'Enable notifications for note_events' + }, + { + required: false, + name: :tag_push_events, + type: Boolean, + desc: 'Enable notifications for tag_push_events' + }, + { + required: false, + name: :pipeline_events, + type: Boolean, + desc: 'Enable notifications for pipeline_events' + }, + { + required: false, + name: :wiki_page_events, + type: Boolean, + desc: 'Enable notifications for wiki_page_events' + } + ].freeze + end + + def self.services + { + 'asana' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'User API token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' + } + ], + 'assembla' => [ + { + required: true, + name: :token, + type: String, + desc: 'The authentication token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Subdomain setting' + } + ], + 'bamboo' => [ + { + required: true, + name: :bamboo_url, + type: String, + desc: 'Bamboo root URL like https://bamboo.example.com' + }, + { + required: true, + name: :build_key, + type: String, + desc: 'Bamboo build plan key like' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with API access, if applicable' + }, + { + required: true, + name: :password, + type: String, + desc: 'Password of the user' + } + ], + 'bugzilla' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'buildkite' => [ + { + required: true, + name: :token, + type: String, + desc: 'Buildkite project GitLab token' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The buildkite project URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'campfire' => [ + { + required: true, + name: :token, + type: String, + desc: 'Campfire token' + }, + { + required: false, + name: :subdomain, + type: String, + desc: 'Campfire subdomain' + }, + { + required: false, + name: :room, + type: String, + desc: 'Campfire room' + } + ], + 'custom-issue-tracker' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'New issue URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'Issues URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'Project URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'Description' + }, + { + required: false, + name: :title, + type: String, + desc: 'Title' + } + ], + 'discord' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…' + } + ], + 'drone-ci' => [ + { + required: true, + name: :token, + type: String, + desc: 'Drone CI token' + }, + { + required: true, + name: :drone_url, + type: String, + desc: 'Drone CI URL' + }, + { + required: false, + name: :enable_ssl_verification, + type: Boolean, + desc: 'Enable SSL verification for communication' + } + ], + 'emails-on-push' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :disable_diffs, + type: Boolean, + desc: 'Disable code diffs' + }, + { + required: false, + name: :send_from_committer_email, + type: Boolean, + desc: 'Send from committer' + } + ], + 'external-wiki' => [ + { + required: true, + name: :external_wiki_url, + type: String, + desc: 'The URL of the external Wiki' + } + ], + 'flowdock' => [ + { + required: true, + name: :token, + type: String, + desc: 'Flowdock token' + } + ], + 'hangouts-chat' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…' + }, + chat_notification_events + ].flatten, + 'irker' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Recipients/channels separated by whitespaces' + }, + { + required: false, + name: :default_irc_uri, + type: String, + desc: 'Default: irc://irc.network.net:6697' + }, + { + required: false, + name: :server_host, + type: String, + desc: 'Server host. Default localhost' + }, + { + required: false, + name: :server_port, + type: Integer, + desc: 'Server port. Default 6659' + }, + { + required: false, + name: :colorize_messages, + type: Boolean, + desc: 'Colorize messages' + } + ], + 'jira' => [ + { + required: true, + name: :url, + type: String, + desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com' + }, + { + required: false, + name: :api_url, + type: String, + desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' + }, + { + required: true, + name: :username, + type: String, + desc: 'The username of the user created to be used with GitLab/JIRA' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user created to be used with GitLab/JIRA' + }, + { + required: false, + name: :jira_issue_transition_id, + type: String, + desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' + } + ], + 'kubernetes' => [ + { + required: true, + name: :namespace, + type: String, + desc: 'The Kubernetes namespace to use' + }, + { + required: true, + name: :api_url, + type: String, + desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' + }, + { + required: true, + name: :token, + type: String, + desc: 'The service token to authenticate against the Kubernetes cluster with' + }, + { + required: false, + name: :ca_pem, + type: String, + desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' + } + ], + 'mattermost-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Mattermost token' + } + ], + 'slack-slash-commands' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Slack token' + } + ], + 'packagist' => [ + { + required: true, + name: :username, + type: String, + desc: 'The username' + }, + { + required: true, + name: :token, + type: String, + desc: 'The Packagist API token' + }, + { + required: false, + name: :server, + type: String, + desc: 'The server' + } + ], + 'pipelines-email' => [ + { + required: true, + name: :recipients, + type: String, + desc: 'Comma-separated list of recipient email addresses' + }, + { + required: false, + name: :notify_only_broken_pipelines, + type: Boolean, + desc: 'Notify only broken pipelines' + } + ], + 'pivotaltracker' => [ + { + required: true, + name: :token, + type: String, + desc: 'The Pivotaltracker token' + }, + { + required: false, + name: :restrict_to_branch, + type: String, + desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' + } + ], + 'prometheus' => [ + { + required: true, + name: :api_url, + type: String, + desc: 'Prometheus API Base URL, like http://prometheus.example.com/' + } + ], + 'pushover' => [ + { + required: true, + name: :api_key, + type: String, + desc: 'The application key' + }, + { + required: true, + name: :user_key, + type: String, + desc: 'The user key' + }, + { + required: true, + name: :priority, + type: String, + desc: 'The priority' + }, + { + required: true, + name: :device, + type: String, + desc: 'Leave blank for all active devices' + }, + { + required: true, + name: :sound, + type: String, + desc: 'The sound of the notification' + } + ], + 'redmine' => [ + { + required: true, + name: :new_issue_url, + type: String, + desc: 'The new issue URL' + }, + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'youtrack' => [ + { + required: true, + name: :project_url, + type: String, + desc: 'The project URL' + }, + { + required: true, + name: :issues_url, + type: String, + desc: 'The issues URL' + }, + { + required: false, + name: :description, + type: String, + desc: 'The description of the tracker' + } + ], + 'slack' => [ + chat_notification_settings, + chat_notification_flags, + chat_notification_channels, + chat_notification_events + ].flatten, + 'microsoft-teams' => [ + { + required: true, + name: :webhook, + type: String, + desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' + } + ], + 'mattermost' => [ + chat_notification_settings, + chat_notification_flags, + chat_notification_channels, + chat_notification_events + ].flatten, + 'teamcity' => [ + { + required: true, + name: :teamcity_url, + type: String, + desc: 'TeamCity root URL like https://teamcity.example.com' + }, + { + required: true, + name: :build_type, + type: String, + desc: 'Build configuration ID' + }, + { + required: true, + name: :username, + type: String, + desc: 'A user with permissions to trigger a manual build' + }, + { + required: true, + name: :password, + type: String, + desc: 'The password of the user' + } + ] + } + end + + def self.service_classes + [ + ::AsanaService, + ::AssemblaService, + ::BambooService, + ::BugzillaService, + ::BuildkiteService, + ::CampfireService, + ::CustomIssueTrackerService, + ::DiscordService, + ::DroneCiService, + ::EmailsOnPushService, + ::ExternalWikiService, + ::FlowdockService, + ::HangoutsChatService, + ::IrkerService, + ::JiraService, + ::KubernetesService, + ::MattermostSlashCommandsService, + ::SlackSlashCommandsService, + ::PackagistService, + ::PipelinesEmailService, + ::PivotaltrackerService, + ::PrometheusService, + ::PushoverService, + ::RedmineService, + ::YoutrackService, + ::SlackService, + ::MattermostService, + ::MicrosoftTeamsService, + ::TeamcityService + ] + end + + def self.development_service_classes + [ + ::MockCiService, + ::MockDeploymentService, + ::MockMonitoringService + ] + end + end + end +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 70b32f7d758..7f4a00f1389 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -15,6 +15,12 @@ module API status code { status: success, message: message }.merge(extra_options).compact end + + def lfs_authentication_url(project) + # This is a separate method so that EE can alter its behaviour more + # easily. + project.http_url_to_repo + end end namespace 'internal' do @@ -81,11 +87,6 @@ module API gl_id: Gitlab::GlId.gl_id(user), gl_username: user&.username, git_config_options: [], - - # This repository_path is a bogus value but gitlab-shell still requires - # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135 - repository_path: '/', - gitaly: gitaly_payload(params[:action]) } @@ -118,7 +119,9 @@ module API raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!") end - Gitlab::LfsToken.new(actor).authentication_payload(project.http_url_to_repo) + Gitlab::LfsToken + .new(actor) + .authentication_payload(lfs_authentication_url(project)) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/notes.rb b/lib/api/notes.rb index f7bd092ce50..416cf39d3ec 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -7,9 +7,7 @@ module API before { authenticate! } - NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze - - NOTEABLE_TYPES.each do |noteable_type| + Helpers::NotesHelpers.noteable_types.each do |noteable_type| parent_type = noteable_type.parent_class.to_s.underscore noteables_str = noteable_type.to_s.underscore.pluralize diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index 0c328f7268e..448bef12cec 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -7,9 +7,7 @@ module API before { authenticate! } - EVENTABLE_TYPES = [Issue, MergeRequest].freeze - - EVENTABLE_TYPES.each do |eventable_type| + Helpers::ResourceLabelEventsHelpers.eventable_types.each do |eventable_type| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize diff --git a/lib/api/search.rb b/lib/api/search.rb index f5db692afe5..f65e810bf90 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -45,6 +45,12 @@ module API def entity SCOPE_ENTITY[params[:scope].to_sym] end + + def verify_search_scope! + # In EE we have additional validation requirements for searches. + # Defining this method here as a noop allows us to easily extend it in + # EE, without having to modify this file directly. + end end resource :search do @@ -55,12 +61,13 @@ module API requires :search, type: String, desc: 'The expression it should be searched for' requires :scope, type: String, - desc: 'The scope of search, available scopes: - projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs', - values: %w(projects issues merge_requests milestones snippet_titles snippet_blobs) + desc: 'The scope of the search', + values: Helpers::SearchHelpers.global_search_scopes use :pagination end get do + verify_search_scope! + present search, with: entity end end @@ -74,12 +81,13 @@ module API requires :search, type: String, desc: 'The expression it should be searched for' requires :scope, type: String, - desc: 'The scope of search, available scopes: - projects, issues, merge_requests, milestones', - values: %w(projects issues merge_requests milestones) + desc: 'The scope of the search', + values: Helpers::SearchHelpers.group_search_scopes use :pagination end get ':id/(-/)search' do + verify_search_scope! + present search(group_id: user_group.id), with: entity end end @@ -93,9 +101,8 @@ module API requires :search, type: String, desc: 'The expression it should be searched for' requires :scope, type: String, - desc: 'The scope of search, available scopes: - issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs', - values: %w(issues merge_requests milestones notes wiki_blobs commits blobs) + desc: 'The scope of the search', + values: Helpers::SearchHelpers.project_search_scopes use :pagination end get ':id/(-/)search' do diff --git a/lib/api/services.rb b/lib/api/services.rb index bda6be51553..bc77fae87fa 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,696 +1,8 @@ # frozen_string_literal: true module API class Services < Grape::API - CHAT_NOTIFICATION_SETTINGS = [ - { - required: true, - name: :webhook, - type: String, - desc: 'The chat webhook' - }, - { - required: false, - name: :username, - type: String, - desc: 'The chat username' - }, - { - required: false, - name: :channel, - type: String, - desc: 'The default chat channel' - } - ].freeze - - CHAT_NOTIFICATION_FLAGS = [ - { - required: false, - name: :notify_only_broken_pipelines, - type: Boolean, - desc: 'Send notifications for broken pipelines' - }, - { - required: false, - name: :notify_only_default_branch, - type: Boolean, - desc: 'Send notifications only for the default branch' - } - ].freeze - - CHAT_NOTIFICATION_CHANNELS = [ - { - required: false, - name: :push_channel, - type: String, - desc: 'The name of the channel to receive push_events notifications' - }, - { - required: false, - name: :issue_channel, - type: String, - desc: 'The name of the channel to receive issues_events notifications' - }, - { - required: false, - name: :confidential_issue_channel, - type: String, - desc: 'The name of the channel to receive confidential_issues_events notifications' - }, - { - required: false, - name: :merge_request_channel, - type: String, - desc: 'The name of the channel to receive merge_requests_events notifications' - }, - { - required: false, - name: :note_channel, - type: String, - desc: 'The name of the channel to receive note_events notifications' - }, - { - required: false, - name: :tag_push_channel, - type: String, - desc: 'The name of the channel to receive tag_push_events notifications' - }, - { - required: false, - name: :pipeline_channel, - type: String, - desc: 'The name of the channel to receive pipeline_events notifications' - }, - { - required: false, - name: :wiki_page_channel, - type: String, - desc: 'The name of the channel to receive wiki_page_events notifications' - } - ].freeze - - CHAT_NOTIFICATION_EVENTS = [ - { - required: false, - name: :push_events, - type: Boolean, - desc: 'Enable notifications for push_events' - }, - { - required: false, - name: :issues_events, - type: Boolean, - desc: 'Enable notifications for issues_events' - }, - { - required: false, - name: :confidential_issues_events, - type: Boolean, - desc: 'Enable notifications for confidential_issues_events' - }, - { - required: false, - name: :merge_requests_events, - type: Boolean, - desc: 'Enable notifications for merge_requests_events' - }, - { - required: false, - name: :note_events, - type: Boolean, - desc: 'Enable notifications for note_events' - }, - { - required: false, - name: :tag_push_events, - type: Boolean, - desc: 'Enable notifications for tag_push_events' - }, - { - required: false, - name: :pipeline_events, - type: Boolean, - desc: 'Enable notifications for pipeline_events' - }, - { - required: false, - name: :wiki_page_events, - type: Boolean, - desc: 'Enable notifications for wiki_page_events' - } - ].freeze - - services = { - 'asana' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'User API token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches' - } - ], - 'assembla' => [ - { - required: true, - name: :token, - type: String, - desc: 'The authentication token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Subdomain setting' - } - ], - 'bamboo' => [ - { - required: true, - name: :bamboo_url, - type: String, - desc: 'Bamboo root URL like https://bamboo.example.com' - }, - { - required: true, - name: :build_key, - type: String, - desc: 'Bamboo build plan key like' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with API access, if applicable' - }, - { - required: true, - name: :password, - type: String, - desc: 'Passord of the user' - } - ], - 'bugzilla' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'buildkite' => [ - { - required: true, - name: :token, - type: String, - desc: 'Buildkite project GitLab token' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The buildkite project URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'campfire' => [ - { - required: true, - name: :token, - type: String, - desc: 'Campfire token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Campfire subdomain' - }, - { - required: false, - name: :room, - type: String, - desc: 'Campfire room' - } - ], - 'custom-issue-tracker' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'Description' - }, - { - required: false, - name: :title, - type: String, - desc: 'Title' - } - ], - 'discord' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…' - } - ], - 'drone-ci' => [ - { - required: true, - name: :token, - type: String, - desc: 'Drone CI token' - }, - { - required: true, - name: :drone_url, - type: String, - desc: 'Drone CI URL' - }, - { - required: false, - name: :enable_ssl_verification, - type: Boolean, - desc: 'Enable SSL verification for communication' - } - ], - 'emails-on-push' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :disable_diffs, - type: Boolean, - desc: 'Disable code diffs' - }, - { - required: false, - name: :send_from_committer_email, - type: Boolean, - desc: 'Send from committer' - } - ], - 'external-wiki' => [ - { - required: true, - name: :external_wiki_url, - type: String, - desc: 'The URL of the external Wiki' - } - ], - 'flowdock' => [ - { - required: true, - name: :token, - type: String, - desc: 'Flowdock token' - } - ], - 'hangouts-chat' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…' - }, - CHAT_NOTIFICATION_EVENTS - ].flatten, - 'irker' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Recipients/channels separated by whitespaces' - }, - { - required: false, - name: :default_irc_uri, - type: String, - desc: 'Default: irc://irc.network.net:6697' - }, - { - required: false, - name: :server_host, - type: String, - desc: 'Server host. Default localhost' - }, - { - required: false, - name: :server_port, - type: Integer, - desc: 'Server port. Default 6659' - }, - { - required: false, - name: :colorize_messages, - type: Boolean, - desc: 'Colorize messages' - } - ], - 'jira' => [ - { - required: true, - name: :url, - type: String, - desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com' - }, - { - required: false, - name: :api_url, - type: String, - desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' - }, - { - required: true, - name: :username, - type: String, - desc: 'The username of the user created to be used with GitLab/JIRA' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user created to be used with GitLab/JIRA' - }, - { - required: false, - name: :jira_issue_transition_id, - type: String, - desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' - } - ], - - 'kubernetes' => [ - { - required: true, - name: :namespace, - type: String, - desc: 'The Kubernetes namespace to use' - }, - { - required: true, - name: :api_url, - type: String, - desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com' - }, - { - required: true, - name: :token, - type: String, - desc: 'The service token to authenticate against the Kubernetes cluster with' - }, - { - required: false, - name: :ca_pem, - type: String, - desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)' - } - ], - 'mattermost-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], - 'slack-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Slack token' - } - ], - 'packagist' => [ - { - required: true, - name: :username, - type: String, - desc: 'The username' - }, - { - required: true, - name: :token, - type: String, - desc: 'The Packagist API token' - }, - { - required: false, - name: :server, - type: String, - desc: 'The server' - } - ], - 'pipelines-email' => [ - { - required: true, - name: :recipients, - type: String, - desc: 'Comma-separated list of recipient email addresses' - }, - { - required: false, - name: :notify_only_broken_pipelines, - type: Boolean, - desc: 'Notify only broken pipelines' - } - ], - 'pivotaltracker' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Pivotaltracker token' - }, - { - required: false, - name: :restrict_to_branch, - type: String, - desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.' - } - ], - 'prometheus' => [ - { - required: true, - name: :api_url, - type: String, - desc: 'Prometheus API Base URL, like http://prometheus.example.com/' - } - ], - 'pushover' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'The application key' - }, - { - required: true, - name: :user_key, - type: String, - desc: 'The user key' - }, - { - required: true, - name: :priority, - type: String, - desc: 'The priority' - }, - { - required: true, - name: :device, - type: String, - desc: 'Leave blank for all active devices' - }, - { - required: true, - name: :sound, - type: String, - desc: 'The sound of the notification' - } - ], - 'redmine' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'The new issue URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' - } - ], - 'youtrack' => [ - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - }, - { - required: false, - name: :description, - type: String, - desc: 'The description of the tracker' - } - ], - 'slack' => [ - CHAT_NOTIFICATION_SETTINGS, - CHAT_NOTIFICATION_FLAGS, - CHAT_NOTIFICATION_CHANNELS, - CHAT_NOTIFICATION_EVENTS - ].flatten, - 'microsoft-teams' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…' - } - ], - 'mattermost' => [ - CHAT_NOTIFICATION_SETTINGS, - CHAT_NOTIFICATION_FLAGS, - CHAT_NOTIFICATION_CHANNELS, - CHAT_NOTIFICATION_EVENTS - ].flatten, - 'teamcity' => [ - { - required: true, - name: :teamcity_url, - type: String, - desc: 'TeamCity root URL like https://teamcity.example.com' - }, - { - required: true, - name: :build_type, - type: String, - desc: 'Build configuration ID' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with permissions to trigger a manual build' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user' - } - ] - } - - service_classes = [ - AsanaService, - AssemblaService, - BambooService, - BugzillaService, - BuildkiteService, - CampfireService, - CustomIssueTrackerService, - DiscordService, - DroneCiService, - EmailsOnPushService, - ExternalWikiService, - FlowdockService, - HangoutsChatService, - IrkerService, - JiraService, - KubernetesService, - MattermostSlashCommandsService, - SlackSlashCommandsService, - PackagistService, - PipelinesEmailService, - PivotaltrackerService, - PrometheusService, - PushoverService, - RedmineService, - YoutrackService, - SlackService, - MattermostService, - MicrosoftTeamsService, - TeamcityService - ] + services = Helpers::ServicesHelpers.services + service_classes = Helpers::ServicesHelpers.service_classes if Rails.env.development? services['mock-ci'] = [ @@ -704,11 +16,7 @@ module API services['mock-deployment'] = [] services['mock-monitoring'] = [] - service_classes += [ - MockCiService, - MockDeploymentService, - MockMonitoringService - ] + service_classes += Helpers::ServicesHelpers.development_service_classes end SERVICES = services.freeze diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b16faffe335..3cb2f69c4ef 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -9,6 +9,11 @@ module API @current_setting ||= (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults) end + + def filter_attributes_using_license(attrs) + # This method will be redefined in EE. + attrs + end end desc 'Get the current application settings' do @@ -156,6 +161,8 @@ module API attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled) end + attrs = filter_attributes_using_license(attrs) + if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute present current_settings, with: Entities::ApplicationSetting else diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 8fc7c7361e1..0e829c5699b 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -13,7 +13,7 @@ module API end params do requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false - requires :token, type: String, desc: 'The unique token of trigger' + requires :token, type: String, desc: 'The unique token of trigger or job token' optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 148deb86c4c..d0d81ebc870 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -7,6 +7,14 @@ module API before { authenticate! } before { authorize! :admin_build, user_project } + helpers do + def filter_variable_parameters(params) + # This method exists so that EE can more easily filter out certain + # parameters, without having to modify the source code directly. + params + end + end + params do requires :id, type: String, desc: 'The ID of a project' end @@ -50,6 +58,7 @@ module API end post ':id/variables' do variable_params = declared_params(include_missing: false) + variable_params = filter_variable_parameters(variable_params) variable = user_project.variables.create(variable_params) @@ -75,6 +84,7 @@ module API break not_found!('Variable') unless variable variable_params = declared_params(include_missing: false).except(:key) + variable_params = filter_variable_parameters(variable_params) if variable.update(variable_params) present variable, with: Entities::Variable diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 6c99e20e7af..3c46eb36cdb 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -780,18 +780,18 @@ rollout 100%: function install_dependencies() { apk add -U openssl curl tar gzip bash ca-certificates git - curl -L -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub - curl -L -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk + curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub + curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk apk add glibc-2.28-r0.apk rm glibc-2.28-r0.apk - curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx + curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx mv linux-amd64/helm /usr/bin/ mv linux-amd64/tiller /usr/bin/ helm version --client tiller -version - curl -L -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" + curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl" chmod +x /usr/bin/kubectl kubectl version --client } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index df69e86c1ad..90dab37a33b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1332,6 +1332,9 @@ msgstr "" msgid "CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly." msgstr "" +msgid "CICD|group enabled" +msgstr "" + msgid "CICD|instance enabled" msgstr "" @@ -3196,6 +3199,9 @@ msgstr "" msgid "Environments|You don't have any environments right now" msgstr "" +msgid "Environments|protected" +msgstr "" + msgid "Epic" msgstr "" @@ -3825,18 +3831,33 @@ msgstr "" msgid "Group: %{group_name}" msgstr "" +msgid "GroupSettings|Auto DevOps pipeline was updated for the group" +msgstr "" + +msgid "GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}" +msgstr "" + msgid "GroupSettings|Badges" msgstr "" msgid "GroupSettings|Customize your group badges." msgstr "" +msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group" +msgstr "" + msgid "GroupSettings|Learn more about badges." msgstr "" msgid "GroupSettings|Prevent sharing a project within %{group} with other groups" msgstr "" +msgid "GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found." +msgstr "" + +msgid "GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}." +msgstr "" + msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup." msgstr "" diff --git a/package.json b/package.json index 632a1b90289..60b3203741e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.54.0", - "@gitlab/ui": "^2.2.2", + "@gitlab/ui": "^2.2.3", "apollo-boost": "^0.3.1", "apollo-client": "^2.5.1", "autosize": "^4.0.0", diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index 4070a225260..ff60e7064bf 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Manage' do + # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/45 + context 'Manage', :quarantine do describe 'Add project member' do it 'user adds project member' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/scripts/build_assets_image b/scripts/build_assets_image index 9afada244c8..b659bd751b2 100755 --- a/scripts/build_assets_image +++ b/scripts/build_assets_image @@ -27,3 +27,9 @@ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY} docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA} +# Also tag the image with GitLab version, if running on a tag pipeline, so +# other projects can simply use that instead of computing the slug. +if [ -n "$CI_COMMIT_TAG" ]; then + docker tag ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME} + docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME} +fi diff --git a/scripts/insert-rspec-profiling-data b/scripts/insert-rspec-profiling-data new file mode 100755 index 00000000000..10e337b9972 --- /dev/null +++ b/scripts/insert-rspec-profiling-data @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +require 'csv' +require 'rspec_profiling' +require 'postgres-copy' + +module RspecProfiling + module Collectors + class PSQL + def establish_connection + # This disables the automatic creation of the database and + # table. In the future, we may want a way to specify the host of + # the database to connect so that we can call #install. + Result.establish_connection(results_url) + end + + def prepared? + connection.data_source_exists?(table) + end + + def results_url + ENV['RSPEC_PROFILING_POSTGRES_URL'] + end + + class Result < ActiveRecord::Base + acts_as_copy_target + end + end + end +end + +def insert_data(path) + puts "#{Time.now} Inserting CI stats..." + + collector = RspecProfiling::Collectors::PSQL.new + collector.install + + files = Dir[File.join(path, "*.csv")] + + files.each do |filename| + puts "#{Time.now} Inserting #{filename}..." + result = RspecProfiling::Collectors::PSQL::Result.copy_from(filename) + puts "#{Time.now} Inserted #{result.cmd_tuples} lines in #{filename}, DB response: #{result.cmd_status}" + end +end + +insert_data('rspec_profiling') if ENV['RSPEC_PROFILING_POSTGRES_URL'].present? diff --git a/scripts/trigger-build b/scripts/trigger-build index 9dbafffddfc..9c5fc3c76a5 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -141,7 +141,7 @@ module Trigger "GITLAB_#{edition}_VERSION" => ENV['CI_COMMIT_REF_NAME'], "GITLAB_VERSION" => ENV['CI_COMMIT_REF_NAME'], "GITLAB_TAG" => ENV['CI_COMMIT_TAG'], - "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_REF_SLUG'], + "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : ENV['CI_COMMIT_REF_SLUG'], "#{edition}_PIPELINE" => 'true' } end diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index 40673d10b91..15eb0a442a6 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -66,4 +66,77 @@ describe Groups::Settings::CiCdController do end end end + + describe 'PATCH #update_auto_devops' do + let(:auto_devops_param) { '1' } + + subject do + patch :update_auto_devops, params: { + group_id: group, + group: { auto_devops_enabled: auto_devops_param } + } + end + + context 'when user does not have enough permission' do + before do + group.add_maintainer(user) + end + + it { is_expected.to have_gitlab_http_status(404) } + end + + context 'when user has enough privileges' do + before do + group.add_owner(user) + end + + it { is_expected.to redirect_to(group_settings_ci_cd_path) } + + context 'when service execution went wrong' do + before do + allow_any_instance_of(Groups::AutoDevopsService).to receive(:execute).and_return(false) + allow_any_instance_of(Group).to receive_message_chain(:errors, :full_messages) + .and_return(['Error 1']) + + subject + end + + it 'returns a flash alert' do + expect(response).to set_flash[:alert] + .to eq("There was a problem updating Auto DevOps pipeline: [\"Error 1\"].") + end + end + + context 'when service execution was successful' do + it 'returns a flash notice' do + subject + + expect(response).to set_flash[:notice] + .to eq('Auto DevOps pipeline was updated for the group') + end + end + + context 'when changing auto devops value' do + before do + subject + + group.reload + end + + context 'when explicitly enabling auto devops' do + it 'should update group attribute' do + expect(group.auto_devops_enabled).to eq(true) + end + end + + context 'when explicitly disabling auto devops' do + let(:auto_devops_param) { '0' } + + it 'should update group attribute' do + expect(group.auto_devops_enabled).to eq(false) + end + end + end + end + end end diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 3b354c0d96b..dcef8571f41 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -36,5 +36,13 @@ FactoryBot.define do trait :nested do parent factory: :group end + + trait :auto_devops_enabled do + auto_devops_enabled true + end + + trait :auto_devops_disabled do + auto_devops_enabled false + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 30d3b22d868..ab185ab3972 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -271,6 +271,10 @@ FactoryBot.define do trait :auto_devops do association :auto_devops, factory: :project_auto_devops end + + trait :auto_devops_disabled do + association :auto_devops, factory: [:project_auto_devops, :disabled] + end end # Project with empty repository diff --git a/spec/features/groups/settings/ci_cd_spec.rb b/spec/features/groups/settings/ci_cd_spec.rb index d422fd18346..0f793dbab6e 100644 --- a/spec/features/groups/settings/ci_cd_spec.rb +++ b/spec/features/groups/settings/ci_cd_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' describe 'Group CI/CD settings' do include WaitForRequests - let(:user) {create(:user)} - let(:group) {create(:group)} + let(:user) { create(:user) } + let(:group) { create(:group) } before do group.add_owner(user) @@ -36,4 +36,45 @@ describe 'Group CI/CD settings' do end end end + + describe 'Auto DevOps form' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + context 'as owner first visiting group settings' do + it 'should see instance enabled badge' do + visit group_settings_ci_cd_path(group) + + page.within '#auto-devops-settings' do + expect(page).to have_content('instance enabled') + end + end + end + + context 'when Auto DevOps group has been enabled' do + it 'should see group enabled badge' do + group.update!(auto_devops_enabled: true) + + visit group_settings_ci_cd_path(group) + + page.within '#auto-devops-settings' do + expect(page).to have_content('group enabled') + end + end + end + + context 'when Auto DevOps group has been disabled' do + it 'should not see a badge' do + group.update!(auto_devops_enabled: false) + + visit group_settings_ci_cd_path(group) + + page.within '#auto-devops-settings' do + expect(page).not_to have_content('instance enabled') + expect(page).not_to have_content('group enabled') + end + end + end + end end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 4c85abe9971..bf0c0de89b2 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -110,6 +110,37 @@ describe "Projects > Settings > Pipelines settings" do expect(page).not_to have_content('instance enabled') end end + + context 'when auto devops is turned on group level' do + before do + project.update!(namespace: create(:group, :auto_devops_enabled)) + end + + it 'renders group enabled badge' do + visit project_settings_ci_cd_path(project) + + page.within '#autodevops-settings' do + expect(page).to have_content('group enabled') + expect(find_field('project_auto_devops_attributes_enabled')).to be_checked + end + end + end + + context 'when auto devops is turned on group parent level', :nested_groups do + before do + group = create(:group, parent: create(:group, :auto_devops_enabled)) + project.update!(namespace: group) + end + + it 'renders group enabled badge' do + visit project_settings_ci_cd_path(project) + + page.within '#autodevops-settings' do + expect(page).to have_content('group enabled') + expect(find_field('project_auto_devops_attributes_enabled')).to be_checked + end + end + end end end diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index 223e562238d..d2540696b17 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -29,11 +29,11 @@ describe AutoDevopsHelper do end context 'when the banner is disabled by feature flag' do - it 'allows the feature flag to disable' do + before do Feature.get(:auto_devops_banner_disabled).enable - - expect(subject).to be(false) end + + it { is_expected.to be_falsy } end context 'when dismissed' do @@ -90,4 +90,136 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end end + + describe '#badge_for_auto_devops_scope' do + subject { helper.badge_for_auto_devops_scope(receiver) } + + context 'when receiver is a group' do + context 'when explicitly enabled' do + let(:receiver) { create(:group, :auto_devops_enabled) } + + it { is_expected.to eq('group enabled') } + end + + context 'when explicitly disabled' do + let(:receiver) { create(:group, :auto_devops_disabled) } + + it { is_expected.to be_nil } + end + + context 'when auto devops is implicitly enabled' do + let(:receiver) { create(:group) } + + context 'by instance' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it { is_expected.to eq('instance enabled') } + end + + context 'with groups', :nested_groups do + before do + receiver.update(parent: parent) + end + + context 'when auto devops is enabled on parent' do + let(:parent) { create(:group, :auto_devops_enabled) } + + it { is_expected.to eq('group enabled') } + end + + context 'when auto devops is enabled on parent group' do + let(:root_parent) { create(:group, :auto_devops_enabled) } + let(:parent) { create(:group, parent: root_parent) } + + it { is_expected.to eq('group enabled') } + end + + context 'when auto devops disabled set on parent group' do + let(:root_parent) { create(:group, :auto_devops_disabled) } + let(:parent) { create(:group, parent: root_parent) } + + it { is_expected.to be_nil } + end + end + end + end + + context 'when receiver is a project' do + context 'when auto devops is enabled at project level' do + let(:receiver) { create(:project, :auto_devops) } + + it { is_expected.to be_nil } + end + + context 'when auto devops is disabled at project level' do + let(:receiver) { create(:project, :auto_devops_disabled) } + + it { is_expected.to be_nil } + end + + context 'when auto devops is implicitly enabled' do + let(:receiver) { create(:project) } + + context 'by instance' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it { is_expected.to eq('instance enabled') } + end + + context 'with groups', :nested_groups do + let(:receiver) { create(:project, :repository, namespace: group) } + + before do + stub_application_setting(auto_devops_enabled: false) + end + + context 'when auto devops is enabled on group level' do + let(:group) { create(:group, :auto_devops_enabled) } + + it { is_expected.to eq('group enabled') } + end + + context 'when auto devops is enabled on root group' do + let(:root_parent) { create(:group, :auto_devops_enabled) } + let(:group) { create(:group, parent: root_parent) } + + it { is_expected.to eq('group enabled') } + end + end + end + + context 'when auto devops is implicitly disabled' do + let(:receiver) { create(:project) } + + context 'by instance' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it { is_expected.to be_nil } + end + + context 'with groups', :nested_groups do + let(:receiver) { create(:project, :repository, namespace: group) } + + context 'when auto devops is disabled on group level' do + let(:group) { create(:group, :auto_devops_disabled) } + + it { is_expected.to be_nil } + end + + context 'when root group is enabled and parent disabled' do + let(:root_parent) { create(:group, :auto_devops_enabled) } + let(:group) { create(:group, :auto_devops_disabled, parent: root_parent) } + + it { is_expected.to be_nil } + end + end + end + end + end end diff --git a/spec/javascripts/fixtures/emojis.rb b/spec/javascripts/fixtures/emojis.rb index f5fb008c7b3..4dab697e5e2 100644 --- a/spec/javascripts/fixtures/emojis.rb +++ b/spec/javascripts/fixtures/emojis.rb @@ -9,6 +9,8 @@ describe 'Emojis (JavaScript fixtures)' do it 'emojis/emojis.json' do |example| JavaScriptFixturesHelpers::FIXTURE_PATHS.each do |fixture_path| + next unless File.directory?(fixture_path) + # Copying the emojis.json from the public folder fixture_file_name = File.expand_path('emojis/emojis.json', fixture_path) FileUtils.mkdir_p(File.dirname(fixture_file_name)) diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb index efd87173b9c..2500e2f8333 100644 --- a/spec/migrations/add_foreign_keys_to_todos_spec.rb +++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb @@ -36,7 +36,7 @@ describe AddForeignKeysToTodos, :migration do end context 'add foreign key on note_id' do - let(:note) { create(:note) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:note) { table(:notes).create! } let!(:todo_with_note) { create_todo(note_id: note.id) } let!(:todo_with_invalid_note) { create_todo(note_id: 4711) } let!(:todo_without_note) { create_todo(note_id: nil) } diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb index 19f06810e54..09c78d02890 100644 --- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb +++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb @@ -3,12 +3,30 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb') -describe CalculateConvDevIndexPercentages, :delete do +describe CalculateConvDevIndexPercentages, :migration do let(:migration) { described_class.new } let!(:conv_dev_index) do - create(:conversational_development_index_metric, # rubocop:disable RSpec/FactoriesInMigrationSpecs + table(:conversational_development_index_metrics).create!( + leader_issues: 9.256, leader_notes: 0, + leader_milestones: 16.2456, + leader_boards: 5.2123, + leader_merge_requests: 1.2, + leader_ci_pipelines: 12.1234, + leader_environments: 3.3333, + leader_deployments: 1.200, + leader_projects_prometheus_active: 0.111, + leader_service_desk_issues: 15.891, + instance_issues: 1.234, + instance_notes: 28.123, instance_milestones: 0, + instance_boards: 3.254, + instance_merge_requests: 0.6, + instance_ci_pipelines: 2.344, + instance_environments: 2.2222, + instance_deployments: 0.771, + instance_projects_prometheus_active: 0.109, + instance_service_desk_issues: 13.345, percentage_issues: 0, percentage_notes: 0, percentage_milestones: 0, diff --git a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb index 8f40ac3e38b..0e6bded29b4 100644 --- a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb @@ -1,20 +1,17 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb') -describe CleanupNonexistingNamespacePendingDeleteProjects do - before do - # Stub after_save callbacks that will fail when Project has invalid namespace - allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil) - allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil) - end +describe CleanupNonexistingNamespacePendingDeleteProjects, :migration do + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } describe '#up' do - set(:some_project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let!(:some_project) { projects.create! } + let(:namespace) { namespaces.create!(name: 'test', path: 'test') } it 'only cleans up when namespace does not exist' do - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs - project = build(:project, pending_delete: true, namespace: nil, namespace_id: Namespace.maximum(:id).to_i.succ) # rubocop:disable RSpec/FactoriesInMigrationSpecs - project.save(validate: false) + projects.create!(pending_delete: true, namespace_id: namespace.id) + project = projects.create!(pending_delete: true, namespace_id: 0) expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]]) @@ -22,7 +19,7 @@ describe CleanupNonexistingNamespacePendingDeleteProjects do end it 'does nothing when no pending delete projects without namespace found' do - create(:project, pending_delete: true, namespace: create(:namespace)) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(pending_delete: true, namespace_id: namespace.id) expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async) diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb index 495e86ee888..71a4e71ac8a 100644 --- a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb +++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb @@ -1,20 +1,19 @@ require 'spec_helper' require Rails.root.join('db', 'migrate', '20171106151218_issues_moved_to_id_foreign_key.rb') -# The schema version has to be far enough in advance to have the -# only_mirror_protected_branches column in the projects table to create a -# project via FactoryBot. -describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do - let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let!(:issue_third) { create(:issue) } # rubocop:disable RSpec/FactoriesInMigrationSpecs +describe IssuesMovedToIdForeignKey, :migration do + let(:issues) { table(:issues) } + + let!(:issue_third) { issues.create! } + let!(:issue_second) { issues.create!(moved_to_id: issue_third.id) } + let!(:issue_first) { issues.create!(moved_to_id: issue_second.id) } subject { described_class.new } it 'removes the orphaned moved_to_id' do subject.down - issue_third.update(moved_to_id: 100000) + issue_third.update!(moved_to_id: 0) subject.up diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb index 1f39ad98fb8..d94ae1e52f5 100644 --- a/spec/migrations/move_personal_snippets_files_spec.rb +++ b/spec/migrations/move_personal_snippets_files_spec.rb @@ -1,12 +1,19 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170612071012_move_personal_snippets_files.rb') -describe MovePersonalSnippetsFiles do +describe MovePersonalSnippetsFiles, :migration do let(:migration) { described_class.new } let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_snippet_files_test") } let(:uploads_dir) { File.join(test_dir, 'uploads') } let(:new_uploads_dir) { File.join(uploads_dir, '-', 'system') } + let(:notes) { table(:notes) } + let(:snippets) { table(:snippets) } + let(:uploads) { table(:uploads) } + + let(:user) { table(:users).create!(email: 'user@example.com', projects_limit: 10) } + let(:project) { table(:projects).create!(name: 'gitlab', namespace_id: 1) } + before do allow(CarrierWave).to receive(:root).and_return(test_dir) allow(migration).to receive(:base_directory).and_return(test_dir) @@ -16,14 +23,14 @@ describe MovePersonalSnippetsFiles do describe "#up" do let(:snippet) do - snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs + snippet = snippets.create!(author_id: user.id) create_upload('picture.jpg', snippet) snippet.update(description: markdown_linking_file('picture.jpg', snippet)) snippet end let(:snippet_with_missing_file) do - snippet = create(:snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs + snippet = snippets.create!(author_id: user.id, project_id: project.id) create_upload('picture.jpg', snippet, create_file: false) snippet.update(description: markdown_linking_file('picture.jpg', snippet)) snippet @@ -62,7 +69,10 @@ describe MovePersonalSnippetsFiles do secret = "secret#{snippet.id}" file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" markdown = markdown_linking_file('picture.jpg', snippet) - note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs + note = notes.create!(noteable_id: snippet.id, + noteable_type: Snippet, + note: "with #{markdown}", + author_id: user.id) migration.up @@ -73,14 +83,14 @@ describe MovePersonalSnippetsFiles do describe "#down" do let(:snippet) do - snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs + snippet = snippets.create!(author_id: user.id) create_upload('picture.jpg', snippet, in_new_path: true) snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true)) snippet end let(:snippet_with_missing_file) do - snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs + snippet = snippets.create!(author_id: user.id) create_upload('picture.jpg', snippet, create_file: false, in_new_path: true) snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true)) snippet @@ -119,7 +129,10 @@ describe MovePersonalSnippetsFiles do markdown = markdown_linking_file('picture.jpg', snippet, in_new_path: true) secret = "secret#{snippet.id}" file_location = "/uploads/personal_snippet/#{snippet.id}/#{secret}/picture.jpg" - note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs + note = notes.create!(noteable_id: snippet.id, + noteable_type: Snippet, + note: "with #{markdown}", + author_id: user.id) migration.down @@ -135,7 +148,7 @@ describe MovePersonalSnippetsFiles do secret = '123456789' filename = 'hello.jpg' - snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs + snippet = snippets.create!(author_id: user.id) path_before = "/uploads/personal_snippet/#{snippet.id}/#{secret}/#{filename}" path_after = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/#{filename}" @@ -161,7 +174,11 @@ describe MovePersonalSnippetsFiles do FileUtils.touch(absolute_path) end - create(:upload, model: snippet, path: "#{secret}/#{filename}", uploader: PersonalFileUploader) # rubocop:disable RSpec/FactoriesInMigrationSpecs + uploads.create!(model_id: snippet.id, + model_type: snippet.class, + path: "#{secret}/#{filename}", + uploader: PersonalFileUploader, + size: 100.kilobytes) end def markdown_linking_file(filename, snippet, in_new_path: false) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9dc32a815d8..16624ce47d0 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -810,4 +810,125 @@ describe Group do it { is_expected.to be_truthy } end end + + describe '#first_auto_devops_config' do + using RSpec::Parameterized::TableSyntax + + let(:group) { create(:group) } + + subject { group.first_auto_devops_config } + + where(:instance_value, :group_value, :config) do + # Instance level enabled + true | nil | { status: true, scope: :instance } + true | true | { status: true, scope: :group } + true | false | { status: false, scope: :group } + + # Instance level disabled + false | nil | { status: false, scope: :instance } + false | true | { status: true, scope: :group } + false | false | { status: false, scope: :group } + end + + with_them do + before do + stub_application_setting(auto_devops_enabled: instance_value) + + group.update_attribute(:auto_devops_enabled, group_value) + end + + it { is_expected.to eq(config) } + end + + context 'with parent groups', :nested_groups do + where(:instance_value, :parent_value, :group_value, :config) do + # Instance level enabled + true | nil | nil | { status: true, scope: :instance } + true | nil | true | { status: true, scope: :group } + true | nil | false | { status: false, scope: :group } + + true | true | nil | { status: true, scope: :group } + true | true | true | { status: true, scope: :group } + true | true | false | { status: false, scope: :group } + + true | false | nil | { status: false, scope: :group } + true | false | true | { status: true, scope: :group } + true | false | false | { status: false, scope: :group } + + # Instance level disable + false | nil | nil | { status: false, scope: :instance } + false | nil | true | { status: true, scope: :group } + false | nil | false | { status: false, scope: :group } + + false | true | nil | { status: true, scope: :group } + false | true | true | { status: true, scope: :group } + false | true | false | { status: false, scope: :group } + + false | false | nil | { status: false, scope: :group } + false | false | true | { status: true, scope: :group } + false | false | false | { status: false, scope: :group } + end + + with_them do + before do + stub_application_setting(auto_devops_enabled: instance_value) + parent = create(:group, auto_devops_enabled: parent_value) + + group.update!( + auto_devops_enabled: group_value, + parent: parent + ) + end + + it { is_expected.to eq(config) } + end + end + end + + describe '#auto_devops_enabled?' do + subject { group.auto_devops_enabled? } + + context 'when auto devops is explicitly enabled on group' do + let(:group) { create(:group, :auto_devops_enabled) } + + it { is_expected.to be_truthy } + end + + context 'when auto devops is explicitly disabled on group' do + let(:group) { create(:group, :auto_devops_disabled) } + + it { is_expected.to be_falsy } + end + + context 'when auto devops is implicitly enabled or disabled' do + before do + stub_application_setting(auto_devops_enabled: false) + + group.update!(parent: parent_group) + end + + context 'when auto devops is enabled on root group' do + let(:root_group) { create(:group, :auto_devops_enabled) } + let(:subgroup) { create(:group, parent: root_group) } + let(:parent_group) { create(:group, parent: subgroup) } + + it { is_expected.to be_truthy } + end + + context 'when auto devops is disabled on root group' do + let(:root_group) { create(:group, :auto_devops_disabled) } + let(:subgroup) { create(:group, parent: root_group) } + let(:parent_group) { create(:group, parent: subgroup) } + + it { is_expected.to be_falsy } + end + + context 'when auto devops is disabled on parent group and enabled on root group' do + let(:root_group) { create(:group, :auto_devops_enabled) } + let(:parent_group) { create(:group, :auto_devops_disabled, parent: root_group) } + + it { is_expected.to be_falsy } + end + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 475fbe56e4d..aadc298ae0b 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -775,4 +775,28 @@ describe Namespace do end end end + + describe '#auto_devops_enabled' do + context 'with users' do + let(:user) { create(:user) } + + subject { user.namespace.auto_devops_enabled? } + + before do + user.namespace.update!(auto_devops_enabled: auto_devops_enabled) + end + + context 'when auto devops is explicitly enabled' do + let(:auto_devops_enabled) { true } + + it { is_expected.to eq(true) } + end + + context 'when auto devops is explicitly disabled' do + let(:auto_devops_enabled) { false } + + it { is_expected.to eq(false) } + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 71bd7972436..5c09faafd83 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3631,12 +3631,36 @@ describe Project do subject { project.auto_devops_enabled? } + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it { is_expected.to be_truthy } + end + + context 'when explicitly disabled' do + before do + create(:project_auto_devops, project: project, enabled: false) + end + + it { is_expected.to be_falsey } + end + context 'when enabled in settings' do before do stub_application_setting(auto_devops_enabled: true) end it { is_expected.to be_truthy } + end + + context 'when disabled in settings' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it { is_expected.to be_falsey } context 'when explicitly enabled' do before do @@ -3648,34 +3672,91 @@ describe Project do context 'when explicitly disabled' do before do - create(:project_auto_devops, project: project, enabled: false) + create(:project_auto_devops, :disabled, project: project) end it { is_expected.to be_falsey } end end - context 'when disabled in settings' do + context 'when force_autodevops_on_by_default is enabled for the project' do + it { is_expected.to be_truthy } + end + + context 'with group parents' do + let(:instance_enabled) { true } + before do - stub_application_setting(auto_devops_enabled: false) + stub_application_setting(auto_devops_enabled: instance_enabled) + project.update!(namespace: parent_group) end - it { is_expected.to be_falsey } + context 'when enabled on parent' do + let(:parent_group) { create(:group, :auto_devops_enabled) } - context 'when explicitly enabled' do - before do - create(:project_auto_devops, project: project) + context 'when auto devops instance enabled' do + it { is_expected.to be_truthy } end - it { is_expected.to be_truthy } + context 'when auto devops instance disabled' do + let(:instance_disabled) { false } + + it { is_expected.to be_truthy } + end end - context 'when force_autodevops_on_by_default is enabled for the project' do - before do - Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100) + context 'when disabled on parent' do + let(:parent_group) { create(:group, :auto_devops_disabled) } + + context 'when auto devops instance enabled' do + it { is_expected.to be_falsy } end - it { is_expected.to be_truthy } + context 'when auto devops instance disabled' do + let(:instance_disabled) { false } + + it { is_expected.to be_falsy } + end + end + + context 'when enabled on root parent', :nested_groups do + let(:parent_group) { create(:group, parent: create(:group, :auto_devops_enabled)) } + + context 'when auto devops instance enabled' do + it { is_expected.to be_truthy } + end + + context 'when auto devops instance disabled' do + let(:instance_disabled) { false } + + it { is_expected.to be_truthy } + end + + context 'when explicitly disabled on parent' do + let(:parent_group) { create(:group, :auto_devops_disabled, parent: create(:group, :auto_devops_enabled)) } + + it { is_expected.to be_falsy } + end + end + + context 'when disabled on root parent', :nested_groups do + let(:parent_group) { create(:group, parent: create(:group, :auto_devops_disabled)) } + + context 'when auto devops instance enabled' do + it { is_expected.to be_falsy } + end + + context 'when auto devops instance disabled' do + let(:instance_disabled) { false } + + it { is_expected.to be_falsy } + end + + context 'when explicitly disabled on parent' do + let(:parent_group) { create(:group, :auto_devops_disabled, parent: create(:group, :auto_devops_enabled)) } + + it { is_expected.to be_falsy } + end end end end @@ -3722,15 +3803,52 @@ describe Project do end end end + + context 'when enabled on group' do + it 'has auto devops implicitly enabled' do + project.update(namespace: create(:group, :auto_devops_enabled)) + + expect(project).to have_auto_devops_implicitly_enabled + end + end + + context 'when enabled on parent group' do + it 'has auto devops implicitly enabled' do + subgroup = create(:group, parent: create(:group, :auto_devops_enabled)) + project.update(namespace: subgroup) + + expect(project).to have_auto_devops_implicitly_enabled + end + end end describe '#has_auto_devops_implicitly_disabled?' do + set(:project) { create(:project) } + before do allow(Feature).to receive(:enabled?).and_call_original Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0) end - set(:project) { create(:project) } + context 'when explicitly disabled' do + before do + create(:project_auto_devops, project: project, enabled: false) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project, enabled: true) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end context 'when enabled in settings' do before do @@ -3753,6 +3871,8 @@ describe Project do context 'when force_autodevops_on_by_default is enabled for the project' do before do + create(:project_auto_devops, project: project, enabled: false) + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100) end @@ -3761,23 +3881,20 @@ describe Project do end end - context 'when explicitly disabled' do - before do - create(:project_auto_devops, project: project, enabled: false) - end + context 'when disabled on group' do + it 'has auto devops implicitly disabled' do + project.update!(namespace: create(:group, :auto_devops_disabled)) - it 'does not have auto devops implicitly disabled' do - expect(project).not_to have_auto_devops_implicitly_disabled + expect(project).to have_auto_devops_implicitly_disabled end end - context 'when explicitly enabled' do - before do - create(:project_auto_devops, project: project, enabled: true) - end + context 'when disabled on parent group' do + it 'has auto devops implicitly disabled' do + subgroup = create(:group, parent: create(:group, :auto_devops_disabled)) + project.update!(namespace: subgroup) - it 'does not have auto devops implicitly disabled' do - expect(project).not_to have_auto_devops_implicitly_disabled + expect(project).to have_auto_devops_implicitly_disabled end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 52ce4d66b4a..b184c92824a 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -26,6 +26,21 @@ describe API::Internal do expect(json_response['redis']).to be(false) end + + context 'authenticating' do + it 'authenticates using a header' do + get api("/internal/check"), + headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns 401 when no credentials provided' do + get(api("/internal/check")) + + expect(response).to have_gitlab_http_status(401) + end + end end describe 'GET /internal/broadcast_message' do @@ -332,7 +347,6 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to be_nil @@ -345,7 +359,6 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to eql(Date.today) @@ -358,7 +371,6 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil @@ -378,7 +390,6 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy - expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil diff --git a/spec/services/groups/auto_devops_service_spec.rb b/spec/services/groups/auto_devops_service_spec.rb new file mode 100644 index 00000000000..7f8ab517cef --- /dev/null +++ b/spec/services/groups/auto_devops_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Groups::AutoDevopsService, '#execute' do + set(:group) { create(:group) } + set(:user) { create(:user) } + let(:group_params) { { auto_devops_enabled: '0' } } + let(:service) { described_class.new(group, user, group_params) } + + context 'when user does not have enough privileges' do + it 'raises exception' do + group.add_developer(user) + + expect do + service.execute + end.to raise_exception(Gitlab::Access::AccessDeniedError) + end + end + + context 'when user has enough privileges' do + before do + group.add_owner(user) + end + + it 'updates group auto devops enabled accordingly' do + service.execute + + expect(group.auto_devops_enabled).to eq(false) + end + + context 'when group has projects' do + it 'reflects changes on projects' do + project_1 = create(:project, namespace: group) + + service.execute + + expect(project_1).not_to have_auto_devops_implicitly_enabled + end + end + + context 'when group has subgroups' do + it 'reflects changes on subgroups' do + subgroup_1 = create(:group, parent: group) + + service.execute + + expect(subgroup_1.auto_devops_enabled?).to eq(false) + end + + context 'when subgroups have projects', :nested_groups do + it 'reflects changes on projects' do + subgroup_1 = create(:group, parent: group) + project_1 = create(:project, namespace: subgroup_1) + + service.execute + + expect(project_1).not_to have_auto_devops_implicitly_enabled + end + end + end + end +end diff --git a/spec/support/pg_stat_activity.rb b/spec/support/pg_stat_activity.rb deleted file mode 100644 index f93fba08a19..00000000000 --- a/spec/support/pg_stat_activity.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -RSpec.configure do |config| - config.before do - if Gitlab::Database.postgresql? && ENV['PG_STAT_WARNING_THRESHOLD'] - warning_threshold = ENV['PG_STAT_WARNING_THRESHOLD'].to_i - results = ActiveRecord::Base.connection.execute('SELECT * FROM pg_stat_activity') - ntuples = results.ntuples - - warn("pg_stat_activity count: #{ntuples}") - - if ntuples > warning_threshold - results.each do |result| - warn result.inspect - end - end - end - end -end diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb index 2a2539c80b5..b52fc719a64 100644 --- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb +++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb @@ -5,6 +5,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do before do assign :project, project + allow(view).to receive(:auto_devops_enabled) { true } end it 'shows a warning message about Kubernetes cluster' do diff --git a/yarn.lock b/yarn.lock index 3cb0fea64fc..4456810ec43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -663,10 +663,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.54.0.tgz#00320e845efd46716042cde0c348b990d4908daf" integrity sha512-DR17iy8TM5IbXEacqiDP0p8SuC/J8EL+98xbfVz5BKvRsPHpeZJQNlBF/petIV5d+KWM5A9v3GZTY7uMU7z/JQ== -"@gitlab/ui@^2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.2.2.tgz#de8f436d6b52c168c4752b221a88cf7665fe0134" - integrity sha512-xVY8lGfDA3D2EtyfZVpJVeNUXLbpn/xJqNF4fleqHJfqrwt+IcVlsQ7yEs/LBukmIw6tXPliD4Mm9uefnQhkXA== +"@gitlab/ui@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.2.3.tgz#b3b4d1d785662dfba44ad2a7354fcbe4cee22fc9" + integrity sha512-N1Q1O6zpS4zZhmvWsUCtuGXTzQeHOzRWQZctbFTEJonidIWk6juqIBduYgR0MadG3DZxiovUN12jDGVtCfZKzw== dependencies: "@babel/standalone" "^7.0.0" bootstrap-vue "^2.0.0-rc.11" |