Merge branch 'master' into sh-add-object-storage-qa
* master: (31 commits)
This commit is contained in:
		
						commit
						6232c26ac7
					
				|  | @ -739,7 +739,7 @@ karma: | |||
|       - chrome_debug.log | ||||
|       - coverage-javascript/ | ||||
| 
 | ||||
| codequality: | ||||
| code_quality: | ||||
|   <<: *dedicated-no-docs-no-db-pull-cache-job | ||||
|   image: docker:stable | ||||
|   allow_failure: true | ||||
|  | @ -757,9 +757,13 @@ codequality: | |||
|   script: | ||||
|     # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products | ||||
|     - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') | ||||
|     - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code | ||||
|     - docker run | ||||
|         --env SOURCE_CODE="$PWD" | ||||
|         --volume "$PWD":/code | ||||
|         --volume /var/run/docker.sock:/var/run/docker.sock | ||||
|         "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code | ||||
|   artifacts: | ||||
|     paths: [codeclimate.json] | ||||
|     paths: [gl-code-quality-report.json] | ||||
|     expire_in: 1 week | ||||
| 
 | ||||
| sast: | ||||
|  |  | |||
|  | @ -64,11 +64,11 @@ As of July 2018, all the documentation for contributing to the GitLab project ha | |||
| 
 | ||||
| ## Contribute to GitLab | ||||
| 
 | ||||
| For a first-time step-by-step guide to the contribution process, see | ||||
| ["Contributing to GitLab"](https://about.gitlab.com/contributing/). | ||||
| 
 | ||||
| Thank you for your interest in contributing to GitLab. This guide details how | ||||
| to contribute to GitLab in a way that is efficient for everyone. | ||||
| to contribute to GitLab in a way that is easy for everyone. | ||||
| 
 | ||||
| For a first-time step-by-step guide to the contribution process, please see | ||||
| ["Contributing to GitLab"](https://about.gitlab.com/contributing/). | ||||
| 
 | ||||
| Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute). | ||||
| 
 | ||||
|  | @ -77,10 +77,10 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial | |||
| edition. Throughout this guide you will see references to CE and EE for | ||||
| abbreviation. | ||||
| 
 | ||||
| If you have read this guide and want to know how the GitLab [core team] | ||||
| If you want to know how the GitLab [core team] | ||||
| operates please see [the GitLab contributing process](PROCESS.md). | ||||
| 
 | ||||
| - [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) | ||||
| [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) | ||||
| 
 | ||||
| ## Security vulnerability disclosure | ||||
| 
 | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| 0.117.1 | ||||
| 0.117.2 | ||||
|  |  | |||
							
								
								
									
										5
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										5
									
								
								Gemfile
								
								
								
								
							|  | @ -214,9 +214,6 @@ gem 'jira-ruby', '~> 1.4' | |||
| # Flowdock integration | ||||
| gem 'gitlab-flowdock-git-hook', '~> 1.0.1' | ||||
| 
 | ||||
| # Gemnasium integration | ||||
| gem 'gemnasium-gitlab-service', '~> 0.2' | ||||
| 
 | ||||
| # Slack integration | ||||
| gem 'slack-notifier', '~> 1.5.1' | ||||
| 
 | ||||
|  | @ -423,7 +420,7 @@ group :ed25519 do | |||
| end | ||||
| 
 | ||||
| # Gitaly GRPC client | ||||
| gem 'gitaly-proto', '~> 0.112.0', require: 'gitaly' | ||||
| gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly' | ||||
| gem 'grpc', '~> 1.11.0' | ||||
| 
 | ||||
| # Locked until https://github.com/google/protobuf/issues/4210 is closed | ||||
|  |  | |||
|  | @ -269,8 +269,6 @@ GEM | |||
|     fuubar (2.2.0) | ||||
|       rspec-core (~> 3.0) | ||||
|       ruby-progressbar (~> 1.4) | ||||
|     gemnasium-gitlab-service (0.2.6) | ||||
|       rugged (~> 0.21) | ||||
|     gemojione (3.3.0) | ||||
|       json | ||||
|     get_process_mem (0.2.0) | ||||
|  | @ -284,7 +282,7 @@ GEM | |||
|       gettext_i18n_rails (>= 0.7.1) | ||||
|       po_to_json (>= 1.0.0) | ||||
|       rails (>= 3.2.0) | ||||
|     gitaly-proto (0.112.0) | ||||
|     gitaly-proto (0.113.0) | ||||
|       google-protobuf (~> 3.1) | ||||
|       grpc (~> 1.10) | ||||
|     github-linguist (5.3.3) | ||||
|  | @ -1040,12 +1038,11 @@ DEPENDENCIES | |||
|   font-awesome-rails (~> 4.7) | ||||
|   foreman (~> 0.84.0) | ||||
|   fuubar (~> 2.2.0) | ||||
|   gemnasium-gitlab-service (~> 0.2) | ||||
|   gemojione (~> 3.3) | ||||
|   gettext (~> 3.2.2) | ||||
|   gettext_i18n_rails (~> 1.8.0) | ||||
|   gettext_i18n_rails_js (~> 1.3) | ||||
|   gitaly-proto (~> 0.112.0) | ||||
|   gitaly-proto (~> 0.113.0) | ||||
|   github-linguist (~> 5.3.3) | ||||
|   gitlab-flowdock-git-hook (~> 1.0.1) | ||||
|   gitlab-gollum-lib (~> 4.2) | ||||
|  |  | |||
|  | @ -272,8 +272,6 @@ GEM | |||
|     fuubar (2.2.0) | ||||
|       rspec-core (~> 3.0) | ||||
|       ruby-progressbar (~> 1.4) | ||||
|     gemnasium-gitlab-service (0.2.6) | ||||
|       rugged (~> 0.21) | ||||
|     gemojione (3.3.0) | ||||
|       json | ||||
|     get_process_mem (0.2.0) | ||||
|  | @ -287,7 +285,7 @@ GEM | |||
|       gettext_i18n_rails (>= 0.7.1) | ||||
|       po_to_json (>= 1.0.0) | ||||
|       rails (>= 3.2.0) | ||||
|     gitaly-proto (0.112.0) | ||||
|     gitaly-proto (0.113.0) | ||||
|       google-protobuf (~> 3.1) | ||||
|       grpc (~> 1.10) | ||||
|     github-linguist (5.3.3) | ||||
|  | @ -1052,12 +1050,11 @@ DEPENDENCIES | |||
|   font-awesome-rails (~> 4.7) | ||||
|   foreman (~> 0.84.0) | ||||
|   fuubar (~> 2.2.0) | ||||
|   gemnasium-gitlab-service (~> 0.2) | ||||
|   gemojione (~> 3.3) | ||||
|   gettext (~> 3.2.2) | ||||
|   gettext_i18n_rails (~> 1.8.0) | ||||
|   gettext_i18n_rails_js (~> 1.3) | ||||
|   gitaly-proto (~> 0.112.0) | ||||
|   gitaly-proto (~> 0.113.0) | ||||
|   github-linguist (~> 5.3.3) | ||||
|   gitlab-flowdock-git-hook (~> 1.0.1) | ||||
|   gitlab-gollum-lib (~> 4.2) | ||||
|  | @ -1216,4 +1213,4 @@ DEPENDENCIES | |||
|   wikicloth (= 0.8.1) | ||||
| 
 | ||||
| BUNDLED WITH | ||||
|    1.16.2 | ||||
|    1.16.3 | ||||
|  |  | |||
|  | @ -58,7 +58,7 @@ You can access a new installation with the login **`root`** and password **`5ive | |||
| 
 | ||||
| ## Contributing | ||||
| 
 | ||||
| GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. | ||||
| GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details. | ||||
| 
 | ||||
| ## Licensing | ||||
| 
 | ||||
|  | @ -66,7 +66,7 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license. | |||
| 
 | ||||
| All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. | ||||
| 
 | ||||
| All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. | ||||
| All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. | ||||
| 
 | ||||
| ## Install a development environment | ||||
| 
 | ||||
|  |  | |||
|  | @ -172,7 +172,7 @@ export default { | |||
| <template> | ||||
|   <div | ||||
|     v-if="!showEmptyState" | ||||
|     class="prometheus-graphs prepend-top-10" | ||||
|     class="prometheus-graphs prepend-top-default" | ||||
|   > | ||||
|     <div class="environments d-flex align-items-center"> | ||||
|       {{ s__('Metrics|Environment') }} | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ | |||
|     &.svg-#{$width} { | ||||
|       img, | ||||
|       svg { | ||||
|         width: #{$width + 'px'}; | ||||
|         max-width: #{$width + 'px'}; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -1,67 +1,39 @@ | |||
| class AutocompleteController < ApplicationController | ||||
|   AWARD_EMOJI_MAX = 100 | ||||
| 
 | ||||
|   skip_before_action :authenticate_user!, only: [:users, :award_emojis] | ||||
|   before_action :load_project, only: [:users] | ||||
|   before_action :load_group, only: [:users] | ||||
| 
 | ||||
|   def users | ||||
|     @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute | ||||
|     project = Autocomplete::ProjectFinder | ||||
|       .new(current_user, params) | ||||
|       .execute | ||||
| 
 | ||||
|     render json: UserSerializer.new.represent(@users) | ||||
|     group = Autocomplete::GroupFinder | ||||
|       .new(current_user, project, params) | ||||
|       .execute | ||||
| 
 | ||||
|     users = Autocomplete::UsersFinder | ||||
|       .new(params: params, current_user: current_user, project: project, group: group) | ||||
|       .execute | ||||
| 
 | ||||
|     render json: UserSerializer.new.represent(users) | ||||
|   end | ||||
| 
 | ||||
|   def user | ||||
|     @user = User.find(params[:id]) | ||||
|     render json: UserSerializer.new.represent(@user) | ||||
|     user = UserFinder.new(params).execute! | ||||
| 
 | ||||
|     render json: UserSerializer.new.represent(user) | ||||
|   end | ||||
| 
 | ||||
|   # Displays projects to use for the dropdown when moving a resource from one | ||||
|   # project to another. | ||||
|   def projects | ||||
|     project = Project.find_by_id(params[:project_id]) | ||||
|     projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) | ||||
|     projects = Autocomplete::MoveToProjectFinder | ||||
|       .new(current_user, params) | ||||
|       .execute | ||||
| 
 | ||||
|     render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) | ||||
|     render json: MoveToProjectSerializer.new.represent(projects) | ||||
|   end | ||||
| 
 | ||||
|   def award_emojis | ||||
|     emoji_with_count = AwardEmoji | ||||
|       .limit(AWARD_EMOJI_MAX) | ||||
|       .where(user: current_user) | ||||
|       .group(:name) | ||||
|       .order('count_all DESC, name ASC') | ||||
|       .count | ||||
| 
 | ||||
|     # Transform from hash to array to guarantee json order | ||||
|     # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 } | ||||
|     #   => [{ name: 'thumbsup' }, { name: 'thumbsdown' }] | ||||
|     render json: emoji_with_count.map { |k, v| { name: k } } | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def load_group | ||||
|     @group ||= begin | ||||
|       if @project.blank? && params[:group_id].present? | ||||
|         group = Group.find(params[:group_id]) | ||||
|         return render_404 unless can?(current_user, :read_group, group) | ||||
| 
 | ||||
|         group | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def load_project | ||||
|     @project ||= begin | ||||
|       if params[:project_id].present? | ||||
|         project = Project.find(params[:project_id]) | ||||
|         return render_404 unless can?(current_user, :read_project, project) | ||||
| 
 | ||||
|         project | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def projects_finder | ||||
|     MoveToProjectFinder.new(current_user) | ||||
|     render json: AwardedEmojiFinder.new(current_user).execute | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Autocomplete | ||||
|   # Finder for retrieving a group to use for autocomplete data sources. | ||||
|   class GroupFinder | ||||
|     attr_reader :current_user, :project, :group_id | ||||
| 
 | ||||
|     # current_user - The currently logged in user, if any. | ||||
|     # project - The Project (if any) to use for the autocomplete data sources. | ||||
|     # params - A Hash containing parameters to use for finding the project. | ||||
|     # | ||||
|     # The following parameters are supported: | ||||
|     # | ||||
|     # * group_id: The ID of the group to find. | ||||
|     def initialize(current_user = nil, project = nil, params = {}) | ||||
|       @current_user = current_user | ||||
|       @project = project | ||||
|       @group_id = params[:group_id] | ||||
|     end | ||||
| 
 | ||||
|     # Attempts to find a Group based on the current group ID. | ||||
|     def execute | ||||
|       return unless project.blank? && group_id.present? | ||||
| 
 | ||||
|       group = Group.find(group_id) | ||||
| 
 | ||||
|       # This removes the need for using `return render_404` and similar patterns | ||||
|       # in controllers that use this finder. | ||||
|       unless Ability.allowed?(current_user, :read_group, group) | ||||
|         raise ActiveRecord::RecordNotFound | ||||
|           .new("Could not find a Group with ID #{group_id}") | ||||
|       end | ||||
| 
 | ||||
|       group | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,35 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Autocomplete | ||||
|   # Finder that retrieves a list of projects that an issue can be moved to. | ||||
|   class MoveToProjectFinder | ||||
|     attr_reader :current_user, :search, :project_id, :offset_id | ||||
| 
 | ||||
|     # current_user - The User object of the user that wants to view the list of | ||||
|     #                projects. | ||||
|     # | ||||
|     # params - A Hash containing additional parameters to set. | ||||
|     # | ||||
|     # The following parameters can be set (as Symbols): | ||||
|     # | ||||
|     # * search: An optional search query to apply to the list of projects. | ||||
|     # * project_id: The ID of a project to exclude from the returned relation. | ||||
|     # * offset_id: The ID of a project to use for pagination. When given, only | ||||
|     #   projects with a lower ID are included in the list. | ||||
|     def initialize(current_user, params = {}) | ||||
|       @current_user = current_user | ||||
|       @search = params[:search] | ||||
|       @project_id = params[:project_id] | ||||
|       @offset_id = params[:offset_id] | ||||
|     end | ||||
| 
 | ||||
|     def execute | ||||
|       current_user | ||||
|         .projects_where_can_admin_issues | ||||
|         .optionally_search(search) | ||||
|         .excluding_project(project_id) | ||||
|         .paginate_in_descending_order_using_id(before: offset_id) | ||||
|         .eager_load_namespace_and_owner | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,35 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Autocomplete | ||||
|   # Finder for retrieving a project to use for autocomplete data sources. | ||||
|   class ProjectFinder | ||||
|     attr_reader :current_user, :project_id | ||||
| 
 | ||||
|     # current_user - The currently logged in user, if any. | ||||
|     # params - A Hash containing parameters to use for finding the project. | ||||
|     # | ||||
|     # The following parameters are supported: | ||||
|     # | ||||
|     # * project_id: The ID of the project to find. | ||||
|     def initialize(current_user = nil, params = {}) | ||||
|       @current_user = current_user | ||||
|       @project_id = params[:project_id] | ||||
|     end | ||||
| 
 | ||||
|     # Attempts to find a Project based on the current project ID. | ||||
|     def execute | ||||
|       return if project_id.blank? | ||||
| 
 | ||||
|       project = Project.find(project_id) | ||||
| 
 | ||||
|       # This removes the need for using `return render_404` and similar patterns | ||||
|       # in controllers that use this finder. | ||||
|       unless Ability.allowed?(current_user, :read_project, project) | ||||
|         raise ActiveRecord::RecordNotFound | ||||
|           .new("Could not find a Project with ID #{project_id}") | ||||
|       end | ||||
| 
 | ||||
|       project | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,85 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Autocomplete | ||||
|   class UsersFinder | ||||
|     # The number of users to display in the results is hardcoded to 20, and | ||||
|     # pagination is not supported. This ensures that performance remains | ||||
|     # consistent and removes the need for implementing keyset pagination to | ||||
|     # ensure good performance. | ||||
|     LIMIT = 20 | ||||
| 
 | ||||
|     attr_reader :current_user, :project, :group, :search, :skip_users, | ||||
|                 :author_id, :todo_filter, :todo_state_filter, | ||||
|                 :filter_by_current_user | ||||
| 
 | ||||
|     def initialize(params:, current_user:, project:, group:) | ||||
|       @current_user = current_user | ||||
|       @project = project | ||||
|       @group = group | ||||
|       @search = params[:search] | ||||
|       @skip_users = params[:skip_users] | ||||
|       @author_id = params[:author_id] | ||||
|       @todo_filter = params[:todo_filter] | ||||
|       @todo_state_filter = params[:todo_state_filter] | ||||
|       @filter_by_current_user = params[:current_user] | ||||
|     end | ||||
| 
 | ||||
|     def execute | ||||
|       items = limited_users | ||||
| 
 | ||||
|       if search.blank? | ||||
|         # Include current user if available to filter by "Me" | ||||
|         items.unshift(current_user) if prepend_current_user? | ||||
| 
 | ||||
|         if prepend_author? && (author = User.find_by_id(author_id)) | ||||
|           items.unshift(author) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       items.uniq | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     # Returns the users based on the input parameters, as an Array. | ||||
|     # | ||||
|     # This method is separate so it is easier to extend in EE. | ||||
|     def limited_users | ||||
|       # When changing the order of these method calls, make sure that | ||||
|       # reorder_by_name() is called _before_ optionally_search(), otherwise | ||||
|       # reorder_by_name will break the ORDER BY applied in optionally_search(). | ||||
|       find_users | ||||
|         .active | ||||
|         .reorder_by_name | ||||
|         .optionally_search(search) | ||||
|         .where_not_in(skip_users) | ||||
|         .limit_to_todo_authors( | ||||
|           user: current_user, | ||||
|           with_todos: todo_filter, | ||||
|           todo_state: todo_state_filter | ||||
|         ) | ||||
|         .limit(LIMIT) | ||||
|         .to_a | ||||
|     end | ||||
| 
 | ||||
|     def prepend_current_user? | ||||
|       filter_by_current_user.present? && current_user | ||||
|     end | ||||
| 
 | ||||
|     def prepend_author? | ||||
|       author_id.present? && current_user | ||||
|     end | ||||
| 
 | ||||
|     def find_users | ||||
|       if project | ||||
|         project.authorized_users.union_with_user(author_id) | ||||
|       elsif group | ||||
|         group.users_with_parents | ||||
|       elsif current_user | ||||
|         User.all | ||||
|       else | ||||
|         User.none | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,68 +0,0 @@ | |||
| class AutocompleteUsersFinder | ||||
|   # The number of users to display in the results is hardcoded to 20, and | ||||
|   # pagination is not supported. This ensures that performance remains | ||||
|   # consistent and removes the need for implementing keyset pagination to ensure | ||||
|   # good performance. | ||||
|   LIMIT = 20 | ||||
| 
 | ||||
|   attr_reader :current_user, :project, :group, :search, :skip_users, | ||||
|               :author_id, :params | ||||
| 
 | ||||
|   def initialize(params:, current_user:, project:, group:) | ||||
|     @current_user = current_user | ||||
|     @project = project | ||||
|     @group = group | ||||
|     @search = params[:search] | ||||
|     @skip_users = params[:skip_users] | ||||
|     @author_id = params[:author_id] | ||||
|     @params = params | ||||
|   end | ||||
| 
 | ||||
|   def execute | ||||
|     items = find_users | ||||
|     items = items.active | ||||
|     items = items.reorder(:name) | ||||
|     items = items.search(search) if search.present? | ||||
|     items = items.where.not(id: skip_users) if skip_users.present? | ||||
|     items = items.limit(LIMIT) | ||||
| 
 | ||||
|     if params[:todo_filter].present? && current_user | ||||
|       items = items.todo_authors(current_user.id, params[:todo_state_filter]) | ||||
|     end | ||||
| 
 | ||||
|     if search.blank? | ||||
|       # Include current user if available to filter by "Me" | ||||
|       if params[:current_user].present? && current_user | ||||
|         items = [current_user, *items].uniq | ||||
|       end | ||||
| 
 | ||||
|       if author_id.present? && current_user | ||||
|         author = User.find_by_id(author_id) | ||||
|         items = [author, *items].uniq if author | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     items | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def find_users | ||||
|     return users_from_project if project | ||||
|     return group.users_with_parents if group | ||||
|     return User.all if current_user | ||||
| 
 | ||||
|     User.none | ||||
|   end | ||||
| 
 | ||||
|   def users_from_project | ||||
|     if author_id.present? | ||||
|       union = Gitlab::SQL::Union | ||||
|         .new([project.authorized_users, User.where(id: author_id)]) | ||||
| 
 | ||||
|       User.from("(#{union.to_sql}) #{User.table_name}") | ||||
|     else | ||||
|       project.authorized_users | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,21 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Class for retrieving information about emoji awarded _by_ a particular user. | ||||
| class AwardedEmojiFinder | ||||
|   attr_reader :current_user | ||||
| 
 | ||||
|   # current_user - The User to generate the data for. | ||||
|   def initialize(current_user = nil) | ||||
|     @current_user = current_user | ||||
|   end | ||||
| 
 | ||||
|   def execute | ||||
|     return [] unless current_user | ||||
| 
 | ||||
|     # We want the resulting data set to be an Array containing the emoji names | ||||
|     # in descending order, based on how often they were awarded. | ||||
|     AwardEmoji | ||||
|       .award_counts_for_user(current_user) | ||||
|       .map { |name, _| { name: name } } | ||||
|   end | ||||
| end | ||||
|  | @ -1,21 +0,0 @@ | |||
| class MoveToProjectFinder | ||||
|   PAGE_SIZE = 50 | ||||
| 
 | ||||
|   def initialize(user) | ||||
|     @user = user | ||||
|   end | ||||
| 
 | ||||
|   def execute(from_project, search: nil, offset_id: nil) | ||||
|     projects = @user.projects_where_can_admin_issues | ||||
|     projects = projects.search(search) if search.present? | ||||
|     projects = projects.excluding_project(from_project) | ||||
|     projects = projects.order_id_desc | ||||
| 
 | ||||
|     # infinite scroll using offset | ||||
|     projects = projects.where('projects.id < ?', offset_id) if offset_id.present? | ||||
|     projects = projects.limit(PAGE_SIZE) | ||||
| 
 | ||||
|     # to ask for Project#name_with_namespace | ||||
|     projects.includes(namespace: :owner) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,26 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # A simple finding for obtaining a single User. | ||||
| # | ||||
| # While using `User.find_by` directly is straightforward, it can lead to a lot | ||||
| # of code duplication. Sometimes we just want to find a user by an ID, other | ||||
| # times we may want to exclude blocked user. By using this finder (and extending | ||||
| # it whenever necessary) we can keep this logic in one place. | ||||
| class UserFinder | ||||
|   attr_reader :params | ||||
| 
 | ||||
|   def initialize(params) | ||||
|     @params = params | ||||
|   end | ||||
| 
 | ||||
|   # Tries to find a User, returning nil if none could be found. | ||||
|   def execute | ||||
|     User.find_by(id: params[:id]) | ||||
|   end | ||||
| 
 | ||||
|   # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could | ||||
|   # not be found. | ||||
|   def execute! | ||||
|     User.find(params[:id]) | ||||
|   end | ||||
| end | ||||
|  | @ -28,6 +28,23 @@ class AwardEmoji < ActiveRecord::Base | |||
|         .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) | ||||
|         .group('name', 'awardable_id') | ||||
|     end | ||||
| 
 | ||||
|     # Returns the top 100 emoji awarded by the given user. | ||||
|     # | ||||
|     # The returned value is a Hash mapping emoji names to the number of times | ||||
|     # they were awarded: | ||||
|     # | ||||
|     #     { 'thumbsup' => 2, 'thumbsdown' => 1 } | ||||
|     # | ||||
|     # user - The User to get the awards for. | ||||
|     # limt - The maximum number of emoji to return. | ||||
|     def award_counts_for_user(user, limit = 100) | ||||
|       limit(limit) | ||||
|         .where(user: user) | ||||
|         .group(:name) | ||||
|         .order('count_all DESC, name ASC') | ||||
|         .count | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def downvote? | ||||
|  |  | |||
|  | @ -0,0 +1,19 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module OptionallySearch | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   module ClassMethods | ||||
|     def search(*) | ||||
|       raise( | ||||
|         NotImplementedError, | ||||
|         'Your model must implement the "search" class method' | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     # Optionally limits a result set to those matching the given search query. | ||||
|     def optionally_search(query = nil) | ||||
|       query.present? ? search(query) : all | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -28,6 +28,7 @@ class Project < ActiveRecord::Base | |||
|   include WithUploads | ||||
|   include BatchDestroyDependentAssociations | ||||
|   include FeatureGate | ||||
|   include OptionallySearch | ||||
|   extend Gitlab::Cache::RequestCache | ||||
| 
 | ||||
|   extend Gitlab::ConfigHelper | ||||
|  | @ -140,7 +141,6 @@ class Project < ActiveRecord::Base | |||
|   has_one :flowdock_service | ||||
|   has_one :assembla_service | ||||
|   has_one :asana_service | ||||
|   has_one :gemnasium_service | ||||
|   has_one :mattermost_slash_commands_service | ||||
|   has_one :mattermost_service | ||||
|   has_one :slack_slash_commands_service | ||||
|  | @ -384,6 +384,26 @@ class Project < ActiveRecord::Base | |||
|                                             only_integer: true, | ||||
|                                             message: 'needs to be beetween 10 minutes and 1 month' } | ||||
| 
 | ||||
|   # Paginates a collection using a `WHERE id < ?` condition. | ||||
|   # | ||||
|   # before - A project ID to use for filtering out projects with an equal or | ||||
|   #      greater ID. If no ID is given, all projects are included. | ||||
|   # | ||||
|   # limit - The maximum number of rows to include. | ||||
|   def self.paginate_in_descending_order_using_id( | ||||
|     before: nil, | ||||
|     limit: Kaminari.config.default_per_page | ||||
|   ) | ||||
|     relation = order_id_desc.limit(limit) | ||||
|     relation = relation.where('projects.id < ?', before) if before | ||||
| 
 | ||||
|     relation | ||||
|   end | ||||
| 
 | ||||
|   def self.eager_load_namespace_and_owner | ||||
|     includes(namespace: :owner) | ||||
|   end | ||||
| 
 | ||||
|   # Returns a collection of projects that is either public or visible to the | ||||
|   # logged in user. | ||||
|   def self.public_or_visible_to_user(user = nil) | ||||
|  |  | |||
|  | @ -1,62 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require "gemnasium/gitlab_service" | ||||
| 
 | ||||
| class GemnasiumService < Service | ||||
|   prop_accessor :token, :api_key | ||||
|   validates :token, :api_key, presence: true, if: :activated? | ||||
|   validate :deprecation_validation | ||||
| 
 | ||||
|   def title | ||||
|     'Gemnasium' | ||||
|   end | ||||
| 
 | ||||
|   def description | ||||
|     'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' | ||||
|   end | ||||
| 
 | ||||
|   def self.to_param | ||||
|     'gemnasium' | ||||
|   end | ||||
| 
 | ||||
|   def fields | ||||
|     [ | ||||
|       { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true }, | ||||
|       { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true } | ||||
|     ] | ||||
|   end | ||||
| 
 | ||||
|   def self.supported_events | ||||
|     %w(push) | ||||
|   end | ||||
| 
 | ||||
|   def deprecated? | ||||
|     true | ||||
|   end | ||||
| 
 | ||||
|   def deprecation_message | ||||
|     "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available." | ||||
|   end | ||||
| 
 | ||||
|   def deprecation_validation | ||||
|     errors[:base] << deprecation_message | ||||
|   end | ||||
| 
 | ||||
|   def execute(data) | ||||
|     return unless supported_events.include?(data[:object_kind]) | ||||
| 
 | ||||
|     # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010 | ||||
|     repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do | ||||
|       project.repository.path_to_repo | ||||
|     end | ||||
| 
 | ||||
|     Gemnasium::GitlabService.execute( | ||||
|       ref: data[:ref], | ||||
|       before: data[:before], | ||||
|       after: data[:after], | ||||
|       token: token, | ||||
|       api_key: api_key, | ||||
|       repo: repo_path | ||||
|     ) | ||||
|   end | ||||
| end | ||||
|  | @ -19,6 +19,7 @@ class User < ActiveRecord::Base | |||
|   include BulkMemberAccessLoad | ||||
|   include BlocksJsonSerialization | ||||
|   include WithUploads | ||||
|   include OptionallySearch | ||||
| 
 | ||||
|   DEFAULT_NOTIFICATION_LEVEL = :participating | ||||
| 
 | ||||
|  | @ -253,11 +254,41 @@ class User < ActiveRecord::Base | |||
|   scope :external, -> { where(external: true) } | ||||
|   scope :active, -> { with_state(:active).non_internal } | ||||
|   scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } | ||||
|   scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } | ||||
|   scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } | ||||
|   scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } | ||||
|   scope :confirmed, -> { where.not(confirmed_at: nil) } | ||||
| 
 | ||||
|   # Limits the users to those that have TODOs, optionally in the given state. | ||||
|   # | ||||
|   # user - The user to get the todos for. | ||||
|   # | ||||
|   # with_todos - If we should limit the result set to users that are the | ||||
|   #              authors of todos. | ||||
|   # | ||||
|   # todo_state - An optional state to require the todos to be in. | ||||
|   def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil) | ||||
|     if user && with_todos | ||||
|       where(id: Todo.where(user: user, state: todo_state).select(:author_id)) | ||||
|     else | ||||
|       all | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   # Returns a relation that optionally includes the given user. | ||||
|   # | ||||
|   # user_id - The ID of the user to include. | ||||
|   def self.union_with_user(user_id = nil) | ||||
|     if user_id.present? | ||||
|       union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)]) | ||||
| 
 | ||||
|       # We use "unscoped" here so that any inner conditions are not repeated for | ||||
|       # the outer query, which would be redundant. | ||||
|       User.unscoped.from("(#{union.to_sql}) #{User.table_name}") | ||||
|     else | ||||
|       all | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def self.with_two_factor_indistinct | ||||
|     joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") | ||||
|       .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) | ||||
|  | @ -365,6 +396,18 @@ class User < ActiveRecord::Base | |||
|       ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) | ||||
|     end | ||||
| 
 | ||||
|     # Limits the result set to users _not_ in the given query/list of IDs. | ||||
|     # | ||||
|     # users - The list of users to ignore. This can be an | ||||
|     #         `ActiveRecord::Relation`, or an Array. | ||||
|     def where_not_in(users = nil) | ||||
|       users ? where.not(id: users) : all | ||||
|     end | ||||
| 
 | ||||
|     def reorder_by_name | ||||
|       reorder(:name) | ||||
|     end | ||||
| 
 | ||||
|     # searches user by given pattern | ||||
|     # it compares name, email, username fields and user's secondary emails with given pattern | ||||
|     # This method uses ILIKE on PostgreSQL and LIKE on MySQL. | ||||
|  |  | |||
|  | @ -0,0 +1,6 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class MoveToProjectEntity < Grape::Entity | ||||
|   expose :id | ||||
|   expose :name_with_namespace | ||||
| end | ||||
|  | @ -0,0 +1,5 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class MoveToProjectSerializer < BaseSerializer | ||||
|   entity MoveToProjectEntity | ||||
| end | ||||
|  | @ -49,7 +49,7 @@ | |||
|         = submit_tag 'Search', class: 'btn' | ||||
| 
 | ||||
|     .float-right.light | ||||
|       Runners with last contact more than a minute ago: #{@active_runners_cnt} | ||||
|       Runners currently online: #{@active_runners_cnt} | ||||
| 
 | ||||
|   %br | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,11 +2,4 @@ | |||
| - page_title "Metrics for environment", @environment.name | ||||
| 
 | ||||
| .prometheus-container{ class: container_class } | ||||
|   .top-area | ||||
|     .row | ||||
|       .col-sm-6 | ||||
|         %h3 | ||||
|           Environment: | ||||
|           = link_to @environment.name, environment_path(@environment) | ||||
| 
 | ||||
|   #prometheus-graphs{ data: metrics_data(@project, @environment) } | ||||
|  |  | |||
|  | @ -13,4 +13,4 @@ | |||
|   %h4.underlined-title Available specific runners | ||||
|   %ul.bordered-list.available-specific-runners | ||||
|     = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner | ||||
|   = paginate @assignable_runners, theme: "gitlab" | ||||
|   = paginate @assignable_runners, theme: "gitlab", :params => { :anchor => '#js-runners-settings' } | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Does not collapse runners section when using pagination | ||||
| merge_request: | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: "#47845 Add failure_reason to job webhook" | ||||
| merge_request: 21143 | ||||
| author: matemaciek | ||||
| type: added | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Fixes SVGs for empty states in job page overflowing on mobile | ||||
| merge_request: | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Allow spaces in wiki markdown links when using CommonMark | ||||
| merge_request: 20417 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Fix merge requests not showing any diff files for big patches | ||||
| merge_request: 21125 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Remove redundant header from metrics page | ||||
| merge_request: 21282 | ||||
| author: | ||||
| type: changed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Remove Gemnasium service | ||||
| merge_request: 21185 | ||||
| author: | ||||
| type: removed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: 'Events API now requires the read_user or api scope.' | ||||
| merge_request: 20627 | ||||
| author: Warren Parad | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Clarify current runners online text | ||||
| merge_request: 21151 | ||||
| author: Ben Bodenmiller | ||||
| type: other | ||||
|  | @ -25,11 +25,11 @@ for each GitLab application server in your environment. | |||
|    options. Here is an example snippet to add to `/etc/fstab`: | ||||
| 
 | ||||
|     ``` | ||||
|     10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 | ||||
|     ``` | ||||
| 
 | ||||
| 1. Create the shared directories. These may be different depending on your NFS | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ Registry, etc. | |||
| ## Hashed Storage | ||||
| 
 | ||||
| > **Warning:** Hashed storage is in **Beta**. For the latest updates, check the | ||||
| > associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821) | ||||
| > associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/3542) | ||||
| > and please report any problems you encounter. | ||||
| 
 | ||||
| Hashed Storage is the new storage behavior we are rolling out with 10.0. Instead | ||||
|  |  | |||
|  | @ -48,9 +48,11 @@ GitLab removes events older than 1 year from the events table for performance re | |||
| 
 | ||||
| ## List currently authenticated user's events | ||||
| 
 | ||||
| >**Note:** This endpoint was introduced in GitLab 9.3. | ||||
| >**Notes:** | ||||
| > This endpoint was introduced in GitLab 9.3. | ||||
| > `read_user` access was introduced in GitLab 11.3. | ||||
| 
 | ||||
| Get a list of events for the authenticated user. | ||||
| Get a list of events for the authenticated user. Scope `read_user` or `api` is required. | ||||
| 
 | ||||
| ``` | ||||
| GET /events | ||||
|  | @ -119,9 +121,11 @@ Example response: | |||
| 
 | ||||
| ### Get user contribution events | ||||
| 
 | ||||
| >**Note:** Documentation was formerly located in the [Users API pages][users-api]. | ||||
| >**Notes:** | ||||
| > Documentation was formerly located in the [Users API pages][users-api]. | ||||
| > `read_user` access was introduced in GitLab 11.3. | ||||
| 
 | ||||
| Get the contribution events for the specified user, sorted from newest to oldest. | ||||
| Get the contribution events for the specified user, sorted from newest to oldest. Scope `read_user` or `api` is required. | ||||
| 
 | ||||
| ``` | ||||
| GET /users/:id/events | ||||
|  |  | |||
|  | @ -401,48 +401,6 @@ Get Flowdock service settings for a project. | |||
| GET /projects/:id/services/flowdock | ||||
| ``` | ||||
| 
 | ||||
| ## Gemnasium | ||||
| 
 | ||||
| Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities. | ||||
| 
 | ||||
| CAUTION: **Warning:** | ||||
| Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been | ||||
| [acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html) | ||||
| in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available. | ||||
| You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html) | ||||
| to keep monitoring your dependencies. | ||||
| 
 | ||||
| ### Create/Edit Gemnasium service | ||||
| 
 | ||||
| Set Gemnasium service for a project. | ||||
| 
 | ||||
| ``` | ||||
| PUT /projects/:id/services/gemnasium | ||||
| ``` | ||||
| 
 | ||||
| Parameters: | ||||
| 
 | ||||
| | Parameter | Type | Required | Description | | ||||
| | --------- | ---- | -------- | ----------- | | ||||
| | `api_key` | string | true | Your personal API KEY on gemnasium.com | | ||||
| | `token`  | string | true | The project's slug on gemnasium.com | | ||||
| 
 | ||||
| ### Delete Gemnasium service | ||||
| 
 | ||||
| Delete Gemnasium service for a project. | ||||
| 
 | ||||
| ``` | ||||
| DELETE /projects/:id/services/gemnasium | ||||
| ``` | ||||
| 
 | ||||
| ### Get Gemnasium service settings | ||||
| 
 | ||||
| Get Gemnasium service settings for a project. | ||||
| 
 | ||||
| ``` | ||||
| GET /projects/:id/services/gemnasium | ||||
| ``` | ||||
| 
 | ||||
| ## Hangouts Chat | ||||
| 
 | ||||
| Google GSuite team collaboration tool. | ||||
|  |  | |||
|  | @ -76,6 +76,8 @@ learn how to leverage its potential even more. | |||
| - [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) | ||||
| - [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or | ||||
|   more Kubernetes clusters to your project | ||||
| - [Interactive web terminal](interactive_web_terminal/index.md) - Open an interactive | ||||
|   web terminal to debug the running jobs | ||||
| 
 | ||||
| ## GitLab CI/CD for Docker | ||||
| 
 | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 35 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 23 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 54 KiB | 
|  | @ -0,0 +1,52 @@ | |||
| # Getting started with interactive web terminals | ||||
| 
 | ||||
| > Introduced in GitLab 11.3. | ||||
| 
 | ||||
| CAUTION: **Warning:** | ||||
| Interactive web terminals are in beta, so they might not work properly and | ||||
| lack features. For more information [follow issue #25990](https://gitlab.com/gitlab-org/gitlab-ce/issues/25990). | ||||
| 
 | ||||
| Interactive web terminals give the user access to a terminal in GitLab for | ||||
| running one-of commands for their CI pipeline. | ||||
| 
 | ||||
| NOTE: **Note:** | ||||
| This is not available for the shared Runners on GitLab.com. | ||||
| To make use of this feature, you need to provide your | ||||
| [own Runner](https://docs.gitlab.com/runner/install/) and properly | ||||
| [configure it](#configuration). | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| Two things need to be configured for the interactive web terminal to work: | ||||
| 
 | ||||
| - The Runner needs to have [`[session_server]` configured | ||||
|   properly][session-server] | ||||
| - Web terminals need to be | ||||
|   [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support) | ||||
| 
 | ||||
| ## Debugging a running job | ||||
| 
 | ||||
| NOTE: **Note:** Not all executors are | ||||
| [supported](https://docs.gitlab.com/runner/executors/#compatibility-chart). | ||||
| 
 | ||||
| Sometimes, when a job is running, things don't go as you would expect, and it | ||||
| would be helpful if one can have a shell to aid debugging. When a job is | ||||
| running, on the right panel you can see a button `debug` that will open the terminal | ||||
| for the current job. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| When clicked, a new tab will open to the terminal page where you can access | ||||
| the terminal and type commands like a normal shell. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| If you have the terminal open and the job has finished with its tasks, the | ||||
| terminal will block the job from finishing for the duration configured in | ||||
| [`[session_server].terminal_max_retention_time`][session-server] until you | ||||
| close the terminal window. | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| [session-server]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section | ||||
|  | @ -0,0 +1,16 @@ | |||
| # Smoke Tests | ||||
| 
 | ||||
| It is imperative in any testing suite that we have Smoke Tests.  In short, smoke tests are will run quick sanity | ||||
| end-to-end functional tests from GitLab QA and are designed to run against the specified environment to ensure that  | ||||
| basic functionality is working. | ||||
| 
 | ||||
| Currently, our suite consists of this basic functionality coverage: | ||||
| 
 | ||||
| - User Login (Standard Auth) | ||||
| - Project Creation | ||||
| - Issue Creation | ||||
| - Merge Request Creation | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| [Return to Testing documentation](index.md) | ||||
|  | @ -120,6 +120,14 @@ running feature tests (i.e. using Capybara) against it. | |||
| The actual test scenarios and steps are [part of GitLab Rails] so that they're | ||||
| always in-sync with the codebase. | ||||
| 
 | ||||
| ### Smoke tests | ||||
| 
 | ||||
| Smoke tests are quick tests that may be run at any time (especially after the pre-deployment migrations). | ||||
| 
 | ||||
| Much like feature tests - these tests run against the UI and ensure that basic functionality is working. | ||||
| 
 | ||||
| > See [Smoke Tests](smoke.md) for more information. | ||||
| 
 | ||||
| Read a separate document about [end-to-end tests](end_to_end_tests.md) to | ||||
| learn more. | ||||
| 
 | ||||
|  |  | |||
|  | @ -120,7 +120,7 @@ gitlabConfigStorageSize: 1Gi | |||
| Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). | ||||
| 
 | ||||
| > **Note:** | ||||
| Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) and [nip.io](http://nip.io) are unlikely to work. | ||||
| Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) are unlikely to work. | ||||
| 
 | ||||
| ## Installing GitLab using the Helm Chart | ||||
| > **Note:** | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ Bitbucket.org account | |||
| 
 | ||||
| ## Project services | ||||
| 
 | ||||
| Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, | ||||
| Integration with services such as Campfire, Flowdock, HipChat, | ||||
| Pivotal Tracker, and Slack are available in the form of a [Project Service][]. | ||||
| 
 | ||||
| [Project Service]: ../user/project/integrations/project_services.md | ||||
|  |  | |||
|  | @ -34,7 +34,6 @@ Click on the service links to see further configuration instructions and details | |||
| | [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | ||||
| | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | ||||
| | Flowdock | Flowdock is a collaboration web app for technical teams | | ||||
| | Gemnasium   _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | ||||
| | [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat | | ||||
| | [HipChat](hipchat.md) | Private group chat and IM | | ||||
| | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | | ||||
|  |  | |||
|  | @ -1102,6 +1102,7 @@ X-Gitlab-Event: Build Hook | |||
|   "build_finished_at": null, | ||||
|   "build_duration": null, | ||||
|   "build_allow_failure": false, | ||||
|   "build_failure_reason": "script_failure", | ||||
|   "project_id": 380, | ||||
|   "project_name": "gitlab-org/gitlab-test", | ||||
|   "user": { | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| module API | ||||
|   class Events < Grape::API | ||||
|     include PaginationParams | ||||
|     include APIGuard | ||||
| 
 | ||||
|     helpers do | ||||
|       params :event_filter_params do | ||||
|  | @ -24,6 +25,8 @@ module API | |||
|     end | ||||
| 
 | ||||
|     resource :events do | ||||
|       allow_access_with_scope :read_user, if: -> (request) { request.get? } | ||||
| 
 | ||||
|       desc "List currently authenticated user's events" do | ||||
|         detail 'This feature was introduced in GitLab 9.3.' | ||||
|         success Entities::Event | ||||
|  | @ -46,6 +49,8 @@ module API | |||
|       requires :id, type: String, desc: 'The ID or Username of the user' | ||||
|     end | ||||
|     resource :users do | ||||
|       allow_access_with_scope :read_user, if: -> (request) { request.get? } | ||||
| 
 | ||||
|       desc 'Get the contribution events of a specified user' do | ||||
|         detail 'This feature was introduced in GitLab 8.13.' | ||||
|         success Entities::Event | ||||
|  |  | |||
|  | @ -354,20 +354,6 @@ module API | |||
|           desc: 'Flowdock token' | ||||
|         } | ||||
|       ], | ||||
|       'gemnasium' => [ | ||||
|         { | ||||
|           required: true, | ||||
|           name: :api_key, | ||||
|           type: String, | ||||
|           desc: 'Your personal API key on gemnasium.com' | ||||
|         }, | ||||
|         { | ||||
|           required: true, | ||||
|           name: :token, | ||||
|           type: String, | ||||
|           desc: "The project's slug on gemnasium.com" | ||||
|         } | ||||
|       ], | ||||
|       'hangouts-chat' => [ | ||||
|         { | ||||
|           required: true, | ||||
|  | @ -695,7 +681,6 @@ module API | |||
|       EmailsOnPushService, | ||||
|       ExternalWikiService, | ||||
|       FlowdockService, | ||||
|       GemnasiumService, | ||||
|       HangoutsChatService, | ||||
|       HipchatService, | ||||
|       IrkerService, | ||||
|  |  | |||
|  | @ -0,0 +1,77 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'uri' | ||||
| 
 | ||||
| module Banzai | ||||
|   module Filter | ||||
|     # HTML Filter for markdown links with spaces in the URLs | ||||
|     # | ||||
|     # Based on Banzai::Filter::AutolinkFilter | ||||
|     # | ||||
|     # CommonMark does not allow spaces in the url portion of a link. | ||||
|     # For example, `[example](page slug)` is not valid.  However, | ||||
|     # in our wikis, we support (via RedCarpet) this type of link, allowing | ||||
|     # wiki pages to be easily linked by their title.  This filter adds that functionality. | ||||
|     # The intent is for this to only be used in Wikis - in general, we want | ||||
|     # to adhere to CommonMark's spec. | ||||
|     # | ||||
|     class SpacedLinkFilter < HTML::Pipeline::Filter | ||||
|       include ActionView::Helpers::TagHelper | ||||
| 
 | ||||
|       # Pattern to match a standard markdown link | ||||
|       # | ||||
|       # Rubular: http://rubular.com/r/z9EAHxYmKI | ||||
|       LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/ | ||||
| 
 | ||||
|       # Text matching LINK_PATTERN inside these elements will not be linked | ||||
|       IGNORE_PARENTS = %w(a code kbd pre script style).to_set | ||||
| 
 | ||||
|       # The XPath query to use for finding text nodes to parse. | ||||
|       TEXT_QUERY = %Q(descendant-or-self::text()[ | ||||
|         not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')}) | ||||
|         and contains(., ']\(') | ||||
|       ]).freeze | ||||
| 
 | ||||
|       def call | ||||
|         return doc if context[:markdown_engine] == :redcarpet | ||||
| 
 | ||||
|         doc.xpath(TEXT_QUERY).each do |node| | ||||
|           content = node.to_html | ||||
| 
 | ||||
|           next unless content.match(LINK_PATTERN) | ||||
| 
 | ||||
|           html = spaced_link_filter(content) | ||||
| 
 | ||||
|           next if html == content | ||||
| 
 | ||||
|           node.replace(html) | ||||
|         end | ||||
| 
 | ||||
|         doc | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def spaced_link_match(link) | ||||
|         match = LINK_PATTERN.match(link) | ||||
|         return link unless match && match[1] && match[2] | ||||
| 
 | ||||
|         # escape the spaces in the url so that it's a valid markdown link, | ||||
|         # then run it through the markdown processor again, let it do its magic | ||||
|         text     = match[1] | ||||
|         new_link = match[2].gsub(' ', '%20') | ||||
|         title    = match[3] ? " \"#{match[3]}\"" : '' | ||||
|         html     = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context) | ||||
| 
 | ||||
|         # link is wrapped in a <p>, so strip that off | ||||
|         html.sub('<p>', '').chomp('</p>') | ||||
|       end | ||||
| 
 | ||||
|       def spaced_link_filter(text) | ||||
|         Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:| | ||||
|           spaced_link_match(link) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -5,6 +5,7 @@ module Banzai | |||
|         @filters ||= begin | ||||
|           super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) | ||||
|                .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) | ||||
|                .insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ module Gitlab | |||
|           build_finished_at: build.finished_at, | ||||
|           build_duration: build.duration, | ||||
|           build_allow_failure: build.allow_failure, | ||||
|           build_failure_reason: build.failure_reason, | ||||
| 
 | ||||
|           # TODO: do we still need it? | ||||
|           project_id: project.id, | ||||
|  |  | |||
|  | @ -226,6 +226,7 @@ module Gitlab | |||
|         @new_file = diff.from_id == BLANK_SHA | ||||
|         @renamed_file = diff.from_path != diff.to_path | ||||
|         @deleted_file = diff.to_id == BLANK_SHA | ||||
|         @too_large = diff.too_large if diff.respond_to?(:too_large) | ||||
| 
 | ||||
|         collapse! if diff.respond_to?(:collapsed) && diff.collapsed | ||||
|       end | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| module Gitlab | ||||
|   module GitalyClient | ||||
|     class Diff | ||||
|       ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze | ||||
|       ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed too_large).freeze | ||||
| 
 | ||||
|       include AttributesBag | ||||
|     end | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ module Gitlab | |||
|           @note = note | ||||
|           @project = project | ||||
|           @client = client | ||||
|           @user_finder = UserFinder.new(project, client) | ||||
|           @user_finder = GithubImport::UserFinder.new(project, client) | ||||
|         end | ||||
| 
 | ||||
|         def execute | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ module Gitlab | |||
|           @issue = issue | ||||
|           @project = project | ||||
|           @client = client | ||||
|           @user_finder = UserFinder.new(project, client) | ||||
|           @user_finder = GithubImport::UserFinder.new(project, client) | ||||
|           @milestone_finder = MilestoneFinder.new(project) | ||||
|           @issuable_finder = GithubImport::IssuableFinder.new(project, issue) | ||||
|         end | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ module Gitlab | |||
|           @note = note | ||||
|           @project = project | ||||
|           @client = client | ||||
|           @user_finder = UserFinder.new(project, client) | ||||
|           @user_finder = GithubImport::UserFinder.new(project, client) | ||||
|         end | ||||
| 
 | ||||
|         def execute | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ module Gitlab | |||
|           @pull_request = pull_request | ||||
|           @project = project | ||||
|           @client = client | ||||
|           @user_finder = UserFinder.new(project, client) | ||||
|           @user_finder = GithubImport::UserFinder.new(project, client) | ||||
|           @milestone_finder = MilestoneFinder.new(project) | ||||
|           @issuable_finder = | ||||
|             GithubImport::IssuableFinder.new(project, pull_request) | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ module Gitlab | |||
|       end | ||||
| 
 | ||||
|       def safe_keys | ||||
|         issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS | ||||
|         issuable_builder.safe_hook_attributes + issuable_builder::SAFE_HOOK_RELATIONS | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
|  |  | |||
|  | @ -1,50 +1,52 @@ | |||
| module Gitlab | ||||
|   module HookData | ||||
|     class IssueBuilder < BaseBuilder | ||||
|       SAFE_HOOK_ATTRIBUTES = %i[ | ||||
|         assignee_id | ||||
|         author_id | ||||
|         closed_at | ||||
|         confidential | ||||
|         created_at | ||||
|         description | ||||
|         due_date | ||||
|         id | ||||
|         iid | ||||
|         last_edited_at | ||||
|         last_edited_by_id | ||||
|         milestone_id | ||||
|         moved_to_id | ||||
|         project_id | ||||
|         relative_position | ||||
|         state | ||||
|         time_estimate | ||||
|         title | ||||
|         updated_at | ||||
|         updated_by_id | ||||
|       ].freeze | ||||
| 
 | ||||
|       SAFE_HOOK_RELATIONS = %i[ | ||||
|         assignees | ||||
|         labels | ||||
|         total_time_spent | ||||
|       ].freeze | ||||
| 
 | ||||
|       def self.safe_hook_attributes | ||||
|         %i[ | ||||
|           assignee_id | ||||
|           author_id | ||||
|           closed_at | ||||
|           confidential | ||||
|           created_at | ||||
|           description | ||||
|           due_date | ||||
|           id | ||||
|           iid | ||||
|           last_edited_at | ||||
|           last_edited_by_id | ||||
|           milestone_id | ||||
|           moved_to_id | ||||
|           project_id | ||||
|           relative_position | ||||
|           state | ||||
|           time_estimate | ||||
|           title | ||||
|           updated_at | ||||
|           updated_by_id | ||||
|         ].freeze | ||||
|       end | ||||
| 
 | ||||
|       alias_method :issue, :object | ||||
| 
 | ||||
|       def build | ||||
|         attrs = { | ||||
|           description: absolute_image_urls(issue.description), | ||||
|           url: Gitlab::UrlBuilder.build(issue), | ||||
|           total_time_spent: issue.total_time_spent, | ||||
|           human_total_time_spent: issue.human_total_time_spent, | ||||
|           human_time_estimate: issue.human_time_estimate, | ||||
|           assignee_ids: issue.assignee_ids, | ||||
|           assignee_id: issue.assignee_ids.first # This key is deprecated | ||||
|             description: absolute_image_urls(issue.description), | ||||
|             url: Gitlab::UrlBuilder.build(issue), | ||||
|             total_time_spent: issue.total_time_spent, | ||||
|             human_total_time_spent: issue.human_total_time_spent, | ||||
|             human_time_estimate: issue.human_time_estimate, | ||||
|             assignee_ids: issue.assignee_ids, | ||||
|             assignee_id: issue.assignee_ids.first # This key is deprecated | ||||
|         } | ||||
| 
 | ||||
|         issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES) | ||||
|           .merge!(attrs) | ||||
|         issue.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) | ||||
|             .merge!(attrs) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -1,33 +1,35 @@ | |||
| module Gitlab | ||||
|   module HookData | ||||
|     class MergeRequestBuilder < BaseBuilder | ||||
|       SAFE_HOOK_ATTRIBUTES = %i[ | ||||
|         assignee_id | ||||
|         author_id | ||||
|         created_at | ||||
|         description | ||||
|         head_pipeline_id | ||||
|         id | ||||
|         iid | ||||
|         last_edited_at | ||||
|         last_edited_by_id | ||||
|         merge_commit_sha | ||||
|         merge_error | ||||
|         merge_params | ||||
|         merge_status | ||||
|         merge_user_id | ||||
|         merge_when_pipeline_succeeds | ||||
|         milestone_id | ||||
|         source_branch | ||||
|         source_project_id | ||||
|         state | ||||
|         target_branch | ||||
|         target_project_id | ||||
|         time_estimate | ||||
|         title | ||||
|         updated_at | ||||
|         updated_by_id | ||||
|       ].freeze | ||||
|       def self.safe_hook_attributes | ||||
|         %i[ | ||||
|           assignee_id | ||||
|           author_id | ||||
|           created_at | ||||
|           description | ||||
|           head_pipeline_id | ||||
|           id | ||||
|           iid | ||||
|           last_edited_at | ||||
|           last_edited_by_id | ||||
|           merge_commit_sha | ||||
|           merge_error | ||||
|           merge_params | ||||
|           merge_status | ||||
|           merge_user_id | ||||
|           merge_when_pipeline_succeeds | ||||
|           milestone_id | ||||
|           source_branch | ||||
|           source_project_id | ||||
|           state | ||||
|           target_branch | ||||
|           target_project_id | ||||
|           time_estimate | ||||
|           title | ||||
|           updated_at | ||||
|           updated_by_id | ||||
|         ].freeze | ||||
|       end | ||||
| 
 | ||||
|       SAFE_HOOK_RELATIONS = %i[ | ||||
|         assignee | ||||
|  | @ -50,8 +52,8 @@ module Gitlab | |||
|           human_time_estimate: merge_request.human_time_estimate | ||||
|         } | ||||
| 
 | ||||
|         merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES) | ||||
|           .merge!(attrs) | ||||
|         merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) | ||||
|             .merge!(attrs) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
							
								
								
									
										12
									
								
								qa/README.md
								
								
								
								
							
							
						
						
									
										12
									
								
								qa/README.md
								
								
								
								
							|  | @ -35,7 +35,7 @@ following call would login to a local [GDK] instance and run all specs in | |||
| `qa/specs/features`: | ||||
| 
 | ||||
| ``` | ||||
| bin/qa Test::Instance http://localhost:3000 | ||||
| bin/qa Test::Instance::All http://localhost:3000 | ||||
| ``` | ||||
| 
 | ||||
| ### Writing tests | ||||
|  | @ -48,14 +48,14 @@ You can also supply specific tests to run as another parameter. For example, to | |||
| run the repository-related specs, you can execute: | ||||
| 
 | ||||
| ``` | ||||
| bin/qa Test::Instance http://localhost qa/specs/features/repository/ | ||||
| bin/qa Test::Instance::All http://localhost qa/specs/features/repository/ | ||||
| ``` | ||||
| 
 | ||||
| Since the arguments would be passed to `rspec`, you could use all `rspec` | ||||
| options there. For example, passing `--backtrace` and also line number: | ||||
| 
 | ||||
| ``` | ||||
| bin/qa Test::Instance http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace | ||||
| bin/qa Test::Instance::All http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace | ||||
| ``` | ||||
| 
 | ||||
| ### Overriding the authenticated user | ||||
|  | @ -67,7 +67,7 @@ If you need to authenticate as a different user, you can provide the | |||
| `GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables: | ||||
| 
 | ||||
| ``` | ||||
| GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com | ||||
| GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance::All https://gitlab.example.com | ||||
| ``` | ||||
| 
 | ||||
| If your user doesn't have permission to default sandbox group | ||||
|  | @ -75,13 +75,13 @@ If your user doesn't have permission to default sandbox group | |||
| `GITLAB_SANDBOX_NAME`: | ||||
| 
 | ||||
| ``` | ||||
| GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com | ||||
| GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com | ||||
| ``` | ||||
| 
 | ||||
| In addition, the `GITLAB_USER_TYPE` can be set to "ldap" to sign in as an LDAP user: | ||||
| 
 | ||||
| ``` | ||||
| GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com | ||||
| GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com | ||||
| ``` | ||||
| 
 | ||||
| All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables). | ||||
|  |  | |||
							
								
								
									
										6
									
								
								qa/qa.rb
								
								
								
								
							
							
						
						
									
										6
									
								
								qa/qa.rb
								
								
								
								
							|  | @ -77,14 +77,16 @@ module QA | |||
|     # | ||||
|     autoload :Bootable, 'qa/scenario/bootable' | ||||
|     autoload :Actable, 'qa/scenario/actable' | ||||
|     autoload :Taggable, 'qa/scenario/taggable' | ||||
|     autoload :Template, 'qa/scenario/template' | ||||
| 
 | ||||
|     ## | ||||
|     # Test scenario entrypoints. | ||||
|     # | ||||
|     module Test | ||||
|       autoload :Instance, 'qa/scenario/test/instance' | ||||
|       module Instance | ||||
|         autoload :All, 'qa/scenario/test/instance/all' | ||||
|         autoload :Smoke, 'qa/scenario/test/instance/smoke' | ||||
|       end | ||||
| 
 | ||||
|       module Integration | ||||
|         autoload :Github, 'qa/scenario/test/integration/github' | ||||
|  |  | |||
|  | @ -4,7 +4,12 @@ module QA | |||
|       class Fork < Factory::Base | ||||
|         dependency Factory::Repository::ProjectPush, as: :push | ||||
| 
 | ||||
|         dependency Factory::Resource::User, as: :user | ||||
|         dependency Factory::Resource::User, as: :user do |user| | ||||
|           if Runtime::Env.forker? | ||||
|             user.username = Runtime::Env.forker_username | ||||
|             user.password = Runtime::Env.forker_password | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         product(:user) { |factory| factory.user } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,28 +4,52 @@ module QA | |||
|   module Factory | ||||
|     module Resource | ||||
|       class User < Factory::Base | ||||
|         attr_accessor :name, :username, :email, :password | ||||
|         attr_reader :unique_id | ||||
|         attr_writer :username, :password, :name, :email | ||||
| 
 | ||||
|         def initialize | ||||
|           @name = "name-#{SecureRandom.hex(8)}" | ||||
|           @username = "username-#{SecureRandom.hex(8)}" | ||||
|           @email = "mail#{SecureRandom.hex(8)}@mail.com" | ||||
|           @password = 'password' | ||||
|           @unique_id = SecureRandom.hex(8) | ||||
|         end | ||||
| 
 | ||||
|         def username | ||||
|           @username ||= "qa-user-#{unique_id}" | ||||
|         end | ||||
| 
 | ||||
|         def password | ||||
|           @password ||= 'password' | ||||
|         end | ||||
| 
 | ||||
|         def name | ||||
|           @name ||= username | ||||
|         end | ||||
| 
 | ||||
|         def email | ||||
|           @email ||= "#{username}@example.com" | ||||
|         end | ||||
| 
 | ||||
|         def credentials_given? | ||||
|           defined?(@username) && defined?(@password) | ||||
|         end | ||||
| 
 | ||||
|         product(:name) { |factory| factory.name } | ||||
| 
 | ||||
|         product(:username) { |factory| factory.username } | ||||
| 
 | ||||
|         product(:email) { |factory| factory.email } | ||||
| 
 | ||||
|         product(:password) { |factory| factory.password } | ||||
| 
 | ||||
|         def fabricate! | ||||
|           Page::Menu::Main.act { sign_out } | ||||
|           Page::Main::Login.act { switch_to_register_tab } | ||||
|           Page::Main::SignUp.perform do |page| | ||||
|             page.sign_up!(name: name, username: username, email: email, password: password) | ||||
|           Page::Menu::Main.perform { |main| main.sign_out } | ||||
| 
 | ||||
|           if credentials_given? | ||||
|             Page::Main::Login.perform do |login| | ||||
|               login.sign_in_using_credentials(self) | ||||
|             end | ||||
|           else | ||||
|             Page::Main::Login.perform do |login| | ||||
|               login.switch_to_register_tab | ||||
|             end | ||||
|             Page::Main::SignUp.perform do |signup| | ||||
|               signup.sign_up!(self) | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ module QA | |||
|       end | ||||
| 
 | ||||
|       def use_default_credentials | ||||
|         self.username = Runtime::User.name | ||||
|         self.username = Runtime::User.username | ||||
|         self.password = Runtime::User.password | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,17 +40,19 @@ module QA | |||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def sign_in_using_credentials | ||||
|         def sign_in_using_credentials(user = nil) | ||||
|           # Don't try to log-in if we're already logged-in | ||||
|           return if Page::Menu::Main.act { has_personal_area?(wait: 0) } | ||||
| 
 | ||||
|           using_wait_time 0 do | ||||
|             set_initial_password_if_present | ||||
| 
 | ||||
|             raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given? | ||||
| 
 | ||||
|             if Runtime::User.ldap_user? | ||||
|               sign_in_using_ldap_credentials | ||||
|             else | ||||
|               sign_in_using_gitlab_credentials | ||||
|               sign_in_using_gitlab_credentials(user || Runtime::User) | ||||
|             end | ||||
|           end | ||||
| 
 | ||||
|  | @ -69,21 +71,30 @@ module QA | |||
|           click_on 'Register' | ||||
|         end | ||||
| 
 | ||||
|         def switch_to_ldap_tab | ||||
|           click_on 'LDAP' | ||||
|         end | ||||
| 
 | ||||
|         def switch_to_standard_tab | ||||
|           click_on 'Standard' | ||||
|         end | ||||
| 
 | ||||
|         private | ||||
| 
 | ||||
|         def sign_in_using_ldap_credentials | ||||
|           click_link 'LDAP' | ||||
|           switch_to_ldap_tab | ||||
| 
 | ||||
|           fill_in :username, with: Runtime::User.ldap_username | ||||
|           fill_in :password, with: Runtime::User.ldap_password | ||||
|           click_button 'Sign in' | ||||
|         end | ||||
| 
 | ||||
|         def sign_in_using_gitlab_credentials | ||||
|           click_link 'Standard' if page.has_content?('LDAP') | ||||
|         def sign_in_using_gitlab_credentials(user) | ||||
|           switch_to_sign_in_tab unless page.has_button?('Sign in') | ||||
|           switch_to_standard_tab if page.has_content?('LDAP') | ||||
| 
 | ||||
|           fill_in :user_login, with: Runtime::User.name | ||||
|           fill_in :user_password, with: Runtime::User.password | ||||
|           fill_in :user_login, with: user.username | ||||
|           fill_in :user_password, with: user.password | ||||
|           click_button 'Sign in' | ||||
|         end | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,12 +11,12 @@ module QA | |||
|           element :register_button, 'submit "Register"' | ||||
|         end | ||||
| 
 | ||||
|         def sign_up!(name:, username:, email:, password:) | ||||
|           fill_in :new_user_name, with: name | ||||
|           fill_in :new_user_username, with: username | ||||
|           fill_in :new_user_email, with: email | ||||
|           fill_in :new_user_email_confirmation, with: email | ||||
|           fill_in :new_user_password, with: password | ||||
|         def sign_up!(user) | ||||
|           fill_in :new_user_name, with: user.name | ||||
|           fill_in :new_user_username, with: user.username | ||||
|           fill_in :new_user_email, with: user.email | ||||
|           fill_in :new_user_email_confirmation, with: user.email | ||||
|           fill_in :new_user_password, with: user.password | ||||
|           click_button 'Register' | ||||
| 
 | ||||
|           Page::Menu::Main.act { has_personal_area? } | ||||
|  |  | |||
|  | @ -23,7 +23,13 @@ module QA | |||
|             # After we fill the key, JS would generate another field so | ||||
|             # we need to use the same index to find the corresponding one. | ||||
|             keys[index].set(key) | ||||
|             all_elements(:ci_variable_input_value)[index].set(value) | ||||
|             node = all_elements(:ci_variable_input_value)[index] | ||||
| 
 | ||||
|             # Simply run `node.set(value)` is too slow for long text here, | ||||
|             # so we need to run JavaScript directly to set the value. | ||||
|             # The code was inspired from: | ||||
|             # https://github.com/teamcapybara/capybara/blob/679548cea10773d45e32808f4d964377cfe5e892/lib/capybara/selenium/node.rb#L217 | ||||
|             execute_script("arguments[0].value = #{value.to_json}", node) | ||||
|           end | ||||
| 
 | ||||
|           def save_variables | ||||
|  |  | |||
|  | @ -39,6 +39,18 @@ module QA | |||
|         ENV['GITLAB_PASSWORD'] | ||||
|       end | ||||
| 
 | ||||
|       def forker? | ||||
|         forker_username && forker_password | ||||
|       end | ||||
| 
 | ||||
|       def forker_username | ||||
|         ENV['GITLAB_FORKER_USERNAME'] | ||||
|       end | ||||
| 
 | ||||
|       def forker_password | ||||
|         ENV['GITLAB_FORKER_PASSWORD'] | ||||
|       end | ||||
| 
 | ||||
|       def ldap_username | ||||
|         ENV['GITLAB_LDAP_USERNAME'] | ||||
|       end | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ module QA | |||
|     module User | ||||
|       extend self | ||||
| 
 | ||||
|       def default_name | ||||
|       def default_username | ||||
|         'root' | ||||
|       end | ||||
| 
 | ||||
|       def name | ||||
|         Runtime::Env.user_username || default_name | ||||
|       def username | ||||
|         Runtime::Env.user_username || default_username | ||||
|       end | ||||
| 
 | ||||
|       def password | ||||
|  |  | |||
|  | @ -1,17 +0,0 @@ | |||
| module QA | ||||
|   module Scenario | ||||
|     module Taggable | ||||
|       # rubocop:disable Gitlab/ModuleWithInstanceVariables | ||||
| 
 | ||||
|       def tags(*tags) | ||||
|         @tags = tags | ||||
|       end | ||||
| 
 | ||||
|       def focus | ||||
|         @tags.to_a | ||||
|       end | ||||
| 
 | ||||
|       # rubocop:enable Gitlab/ModuleWithInstanceVariables | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,15 +1,36 @@ | |||
| module QA | ||||
|   module Scenario | ||||
|     class Template | ||||
|       def self.perform(*args) | ||||
|         new.tap do |scenario| | ||||
|           yield scenario if block_given? | ||||
|           break scenario.perform(*args) | ||||
|       class << self | ||||
|         def perform(*args) | ||||
|           new.tap do |scenario| | ||||
|             yield scenario if block_given? | ||||
|             break scenario.perform(*args) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         def tags(*tags) | ||||
|           @tags = tags | ||||
|         end | ||||
| 
 | ||||
|         def focus | ||||
|           @tags.to_a | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       def perform(*_args) | ||||
|         raise NotImplementedError | ||||
|       def perform(address, *rspec_options) | ||||
|         Runtime::Scenario.define(:gitlab_address, address) | ||||
| 
 | ||||
|         Specs::Runner.perform do |specs| | ||||
|           specs.tty = true | ||||
|           specs.tags = self.class.focus | ||||
|           specs.options = | ||||
|             if rspec_options.any? | ||||
|               rspec_options | ||||
|             else | ||||
|               ::File.expand_path('../specs/features', __dir__) | ||||
|             end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -1,36 +0,0 @@ | |||
| module QA | ||||
|   module Scenario | ||||
|     module Test | ||||
|       ## | ||||
|       # Base class for running the suite against any GitLab instance, | ||||
|       # including staging and on-premises installation. | ||||
|       # | ||||
|       class Instance < Template | ||||
|         include Bootable | ||||
|         extend Taggable | ||||
| 
 | ||||
|         tags :core | ||||
| 
 | ||||
|         def perform(address, *rspec_options) | ||||
|           Runtime::Scenario.define(:gitlab_address, address) | ||||
| 
 | ||||
|           ## | ||||
|           # Perform before hooks, which are different for CE and EE | ||||
|           # | ||||
|           Runtime::Release.perform_before_hooks | ||||
| 
 | ||||
|           Specs::Runner.perform do |specs| | ||||
|             specs.tty = true | ||||
|             specs.tags = self.class.focus | ||||
|             specs.options = | ||||
|               if rspec_options.any? | ||||
|                 rspec_options | ||||
|               else | ||||
|                 ::File.expand_path('../../specs/features', __dir__) | ||||
|               end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,15 @@ | |||
| module QA | ||||
|   module Scenario | ||||
|     module Test | ||||
|       ## | ||||
|       # Base class for running the suite against any GitLab instance, | ||||
|       # including staging and on-premises installation. | ||||
|       # | ||||
|       module Instance | ||||
|         class All < Template | ||||
|           include Bootable | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,17 @@ | |||
| module QA | ||||
|   module Scenario | ||||
|     module Test | ||||
|       module Instance | ||||
|         ## | ||||
|         # Base class for running the suite against any GitLab instance, | ||||
|         # including staging and on-premises installation. | ||||
|         # | ||||
|         class Smoke < Template | ||||
|           include Bootable | ||||
| 
 | ||||
|           tags :smoke | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,13 +1,13 @@ | |||
| require 'securerandom' | ||||
| 
 | ||||
| module QA | ||||
|   describe 'API basics', :core do | ||||
|   describe 'API basics' do | ||||
|     before(:context) do | ||||
|       @api_client = Runtime::API::Client.new(:gitlab) | ||||
|     end | ||||
| 
 | ||||
|     let(:project_name) { "api-basics-#{SecureRandom.hex(8)}" } | ||||
|     let(:sanitized_project_path) { CGI.escape("#{Runtime::User.name}/#{project_name}") } | ||||
|     let(:sanitized_project_path) { CGI.escape("#{Runtime::User.username}/#{project_name}") } | ||||
| 
 | ||||
|     it 'user creates a project with a file and deletes them afterwards' do | ||||
|       create_project_request = Runtime::API::Request.new(@api_client, '/projects') | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'API users', :core do | ||||
|   describe 'API users' do | ||||
|     before(:context) do | ||||
|       @api_client = Runtime::API::Client.new(:gitlab) | ||||
|     end | ||||
|  | @ -14,11 +14,11 @@ module QA | |||
|       end | ||||
| 
 | ||||
|       it 'submit request with a valid user name' do | ||||
|         get request.url, { params: { username: Runtime::User.name } } | ||||
|         get request.url, { params: { username: Runtime::User.username } } | ||||
| 
 | ||||
|         expect_status(200) | ||||
|         expect(json_body).to contain_exactly( | ||||
|           a_hash_including(username: Runtime::User.name) | ||||
|           a_hash_including(username: Runtime::User.username) | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,15 @@ | |||
| module QA | ||||
|   describe 'basic user login', :smoke do | ||||
|     it 'user logs in using basic credentials' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
| 
 | ||||
|       # TODO, since `Signed in successfully` message was removed | ||||
|       # this is the only way to tell if user is signed in correctly. | ||||
|       # | ||||
|       Page::Menu::Main.perform do |menu| | ||||
|         expect(menu).to have_personal_area | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'LDAP user login', :ldap do | ||||
|   describe 'LDAP user login', :orchestrated, :ldap do | ||||
|     before do | ||||
|       Runtime::Env.user_type = 'ldap' | ||||
|     end | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'create a new group', :mattermost do | ||||
|   describe 'create a new group', :orchestrated, :mattermost do | ||||
|     it 'creating a group with a mattermost team' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'logging in to Mattermost', :mattermost do | ||||
|   describe 'logging in to Mattermost', :orchestrated, :mattermost do | ||||
|     it 'can use gitlab oauth' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) do | ||||
|         Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'creates a merge request', :core do | ||||
|   describe 'creates a merge request with milestone' do | ||||
|     it 'user creates a new merge request'  do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  | @ -29,4 +29,25 @@ module QA | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'creates a merge request', :smoke do | ||||
|     it 'user creates a new merge request'  do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
| 
 | ||||
|       current_project = Factory::Resource::Project.fabricate! do |project| | ||||
|         project.name = 'project-with-merge-request' | ||||
|       end | ||||
| 
 | ||||
|       Factory::Resource::MergeRequest.fabricate! do |merge_request| | ||||
|         merge_request.title = 'This is a merge request' | ||||
|         merge_request.description = 'Great feature' | ||||
|         merge_request.project = current_project | ||||
|       end | ||||
| 
 | ||||
|       expect(page).to have_content('This is a merge request') | ||||
|       expect(page).to have_content('Great feature') | ||||
|       expect(page).to have_content(/Opened [\w\s]+ ago/) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'merge request rebase', :core do | ||||
|   describe 'merge request rebase' do | ||||
|     it 'rebases source branch of merge request'  do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'merge request squash commits', :core do | ||||
|   describe 'merge request squash commits' do | ||||
|     it 'when squash commits is marked before merge'  do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'activity page', :core do | ||||
|   describe 'activity page' do | ||||
|     it 'push creates an event in the activity page' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'deploy keys support', :core do | ||||
|   describe 'deploy keys support' do | ||||
|     it 'user adds a deploy key' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'secret variables support', :core do | ||||
|   describe 'secret variables support' do | ||||
|     it 'user adds a secret variable' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| require 'pathname' | ||||
| 
 | ||||
| module QA | ||||
|   describe 'Auto Devops', :kubernetes do | ||||
|   describe 'Auto Devops', :orchestrated, :kubernetes do | ||||
|     after do | ||||
|       @cluster&.remove! | ||||
|     end | ||||
|  | @ -15,6 +15,13 @@ module QA | |||
|         p.description = 'Project with Auto Devops' | ||||
|       end | ||||
| 
 | ||||
|       # Disable code_quality check in Auto DevOps pipeline as it takes | ||||
|       # too long and times out the test | ||||
|       Factory::Resource::SecretVariable.fabricate! do |resource| | ||||
|         resource.key = 'CODE_QUALITY_DISABLED' | ||||
|         resource.value = '1' | ||||
|       end | ||||
| 
 | ||||
|       # Create Auto Devops compatible repo | ||||
|       Factory::Repository::ProjectPush.fabricate! do |push| | ||||
|         push.project = project | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'creates issue', :core do | ||||
|   describe 'creates issue', :smoke do | ||||
|     let(:issue_title) { 'issue title' } | ||||
| 
 | ||||
|     def create_issue | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'create a new project', :core do | ||||
|   describe 'create a new project', :smoke do | ||||
|     it 'user creates a new project' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| require 'digest/sha1' | ||||
| 
 | ||||
| module QA | ||||
|   describe 'cloning code using a deploy key', :core, :docker do | ||||
|   describe 'cloning code using a deploy key', :docker do | ||||
|     def login | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'Project fork', :core do | ||||
|   describe 'Project fork' do | ||||
|     it 'can submit merge requests to upstream master' do | ||||
|       Runtime::Browser.visit(:gitlab, Page::Main::Login) | ||||
|       Page::Main::Login.act { sign_in_using_credentials } | ||||
|  | @ -8,14 +8,12 @@ module QA | |||
|         merge_request.fork_branch = 'feature-branch' | ||||
|       end | ||||
| 
 | ||||
|       Page::Menu::Main.act { sign_out } | ||||
|       Page::Main::Login.act do | ||||
|         switch_to_sign_in_tab | ||||
|         sign_in_using_credentials | ||||
|       end | ||||
|       Page::Menu::Main.perform { |main| main.sign_out } | ||||
|       Page::Main::Login.perform { |login| login.sign_in_using_credentials } | ||||
| 
 | ||||
|       merge_request.visit! | ||||
|       Page::MergeRequest::Show.act { merge! } | ||||
| 
 | ||||
|       Page::MergeRequest::Show.perform { |show| show.merge! } | ||||
| 
 | ||||
|       expect(page).to have_content('The changes were merged') | ||||
|     end | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'user imports a GitHub repo', :core, :github do | ||||
|   describe 'user imports a GitHub repo', :orchestrated, :github do | ||||
|     let(:imported_project) do | ||||
|       Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| | ||||
|         project.name = 'imported-project' | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| module QA | ||||
|   describe 'CI/CD Pipelines', :core, :docker do | ||||
|   describe 'CI/CD Pipelines', :orchestrated, :docker do | ||||
|     let(:executor) { "qa-runner-#{Time.now.to_i}" } | ||||
| 
 | ||||
|     after do | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue