diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 17b2ccd9bf9050efdf57d7800677e87919b9b5b9..8f0916f768f0487bcf8d33827ce2c8dcecb645c1 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.4.3
+0.5.0
diff --git a/VERSION b/VERSION
index d821c124047fbe5b43f55d51158e96205f9f5c48..be3d36737cc46315e0bb47108eda9a9169deebe6 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-9.3.0-pre
+9.4.0-pre
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index ffd8d494a401a508a5cca36ac2d5d56fae07f96b..81267c68cfc240d42789069f4ac5add96db8ae30 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -396,6 +396,7 @@ import PerformanceBar from './performance_bar';
           initSettingsPanels();
           break;
         case 'projects:settings:ci_cd:show':
+        case 'groups:settings:ci_cd:show':
           new gl.ProjectVariables();
           break;
         case 'ci:lints:create':
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 59e0624d94e3b0dc9143ad576f13fefda0ed41c2..7adf17dddb8248e1c56edd57c162bade4a487fb2 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -731,11 +731,11 @@
 
 .merge-request-tabs-holder {
   top: $header-height;
-  z-index: 100;
+  z-index: 200;
   background-color: $white-light;
   border-bottom: 1px solid $border-color;
 
-  @media(min-width: $screen-sm-min) {
+  @media (min-width: $screen-sm-min) {
     position: sticky;
     position: -webkit-sticky;
   }
@@ -770,6 +770,12 @@
     max-width: $limited-layout-width;
     margin-left: auto;
     margin-right: auto;
+
+    .inner-page-scroll-tabs {
+      background-color: $white-light;
+      margin-left: -$gl-padding;
+      padding-left: $gl-padding;
+    }
   }
 }
 
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0142ad8278c38ac82157874e7431b7c159cda8c3
--- /dev/null
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -0,0 +1,24 @@
+module Groups
+  module Settings
+    class CiCdController < Groups::ApplicationController
+      before_action :authorize_admin_pipeline!
+
+      def show
+        define_secret_variables
+      end
+
+      private
+
+      def define_secret_variables
+        @variable = Ci::GroupVariable.new(group: group)
+          .present(current_user: current_user)
+        @variables = group.variables.order_key_asc
+          .map { |variable| variable.present(current_user: current_user) }
+      end
+
+      def authorize_admin_pipeline!
+        return render_404 unless can?(current_user, :admin_pipeline, group)
+      end
+    end
+  end
+end
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..10038ff3ad9b60084add00c752b818c1290e8467
--- /dev/null
+++ b/app/controllers/groups/variables_controller.rb
@@ -0,0 +1,64 @@
+module Groups
+  class VariablesController < Groups::ApplicationController
+    before_action :variable, only: [:show, :update, :destroy]
+    before_action :authorize_admin_build!
+
+    def index
+      redirect_to group_settings_ci_cd_path(group)
+    end
+
+    def show
+    end
+
+    def update
+      if variable.update(variable_params)
+        redirect_to group_variables_path(group),
+                    notice: 'Variable was successfully updated.'
+      else
+        render "show"
+      end
+    end
+
+    def create
+      @variable = group.variables.create(variable_params)
+        .present(current_user: current_user)
+
+      if @variable.persisted?
+        redirect_to group_settings_ci_cd_path(group),
+                    notice: 'Variable was successfully created.'
+      else
+        render "show"
+      end
+    end
+
+    def destroy
+      if variable.destroy
+        redirect_to group_settings_ci_cd_path(group),
+                    status: 302,
+                    notice: 'Variable was successfully removed.'
+      else
+        redirect_to group_settings_ci_cd_path(group),
+                    status: 302,
+                    notice: 'Failed to remove the variable.'
+      end
+    end
+
+    private
+
+    def variable_params
+      params.require(:variable).permit(*variable_params_attributes)
+    end
+
+    def variable_params_attributes
+      %i[key value protected]
+    end
+
+    def variable
+      @variable ||= group.variables.find(params[:id]).present(current_user: current_user)
+    end
+
+    def authorize_admin_build!
+      return render_404 unless can?(current_user, :admin_build, group)
+    end
+  end
+end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 24fe78bc1bd0d62bd062c35fed90a3e8b8d32a82..ea7ceb3eaa5e7977e66d3b1bd6eba2a4e8405596 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -21,7 +21,10 @@ module Projects
       end
 
       def define_secret_variables
-        @variable = Ci::Variable.new
+        @variable = Ci::Variable.new(project: project)
+          .present(current_user: current_user)
+        @variables = project.variables.order_key_asc
+          .map { |variable| variable.present(current_user: current_user) }
       end
 
       def define_triggers_variables
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 326d31ecec201d66024ae15b1d347cfbede8933d..6a8251375649fe54e15891b52cd1c63316902b8d 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,4 +1,5 @@
 class Projects::VariablesController < Projects::ApplicationController
+  before_action :variable, only: [:show, :update, :destroy]
   before_action :authorize_admin_build!
 
   layout 'project_settings'
@@ -8,37 +9,39 @@ class Projects::VariablesController < Projects::ApplicationController
   end
 
   def show
-    @variable = @project.variables.find(params[:id])
   end
 
   def update
-    @variable = @project.variables.find(params[:id])
-
-    if @variable.update_attributes(variable_params)
-      redirect_to project_variables_path(project), notice: 'Variable was successfully updated.'
+    if variable.update(variable_params)
+      redirect_to project_variables_path(project),
+                  notice: 'Variable was successfully updated.'
     else
-      render action: "show"
+      render "show"
     end
   end
 
   def create
-    @variable = @project.variables.new(variable_params)
+    @variable = project.variables.create(variable_params)
+      .present(current_user: current_user)
 
-    if @variable.save
-      flash[:notice] = 'Variables were successfully updated.'
-      redirect_to project_settings_ci_cd_path(project)
+    if @variable.persisted?
+      redirect_to project_settings_ci_cd_path(project),
+                  notice: 'Variable was successfully created.'
     else
       render "show"
     end
   end
 
   def destroy
-    @key = @project.variables.find(params[:id])
-    @key.destroy
-
-    redirect_to project_settings_ci_cd_path(project),
-                status: 302,
-                notice: 'Variable was successfully removed.'
+    if variable.destroy
+      redirect_to project_settings_ci_cd_path(project),
+                  status: 302,
+                  notice: 'Variable was successfully removed.'
+    else
+      redirect_to project_settings_ci_cd_path(project),
+                  status: 302,
+                  notice: 'Failed to remove the variable.'
+    end
   end
 
   private
@@ -50,4 +53,8 @@ class Projects::VariablesController < Projects::ApplicationController
   def variable_params_attributes
     %i[id key value protected _destroy]
   end
+
+  def variable
+    @variable ||= project.variables.find(params[:id]).present(current_user: current_user)
+  end
 end
diff --git a/app/models/blob_viewer/readme.rb b/app/models/blob_viewer/readme.rb
index 75c373a03bbb28c79596723507eaa77d86fd0438..4604a9934a0c9dd7b115d732f25fa71b0283e9f0 100644
--- a/app/models/blob_viewer/readme.rb
+++ b/app/models/blob_viewer/readme.rb
@@ -10,5 +10,11 @@ module BlobViewer
     def visible_to?(current_user)
       can?(current_user, :read_wiki, project)
     end
+
+    def render_error
+      return if project.has_external_wiki? || (project.wiki_enabled? && project.wiki.has_home_page?)
+
+      :no_wiki
+    end
   end
 end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6c2c2e1d3d00577b07b500c7c31ea126e982ec3f..432f3f242eb4e9cd30458b3207ed5165ed392436 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -200,6 +200,7 @@ module Ci
       variables += project.deployment_variables if has_environment?
       variables += yaml_variables
       variables += user_variables
+      variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
       variables += secret_variables(environment: environment)
       variables += trigger_request.user_variables if trigger_request
       variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f64bc245a67e26597d9f5aec1aa9e5374b1b3db2
--- /dev/null
+++ b/app/models/ci/group_variable.rb
@@ -0,0 +1,13 @@
+module Ci
+  class GroupVariable < ActiveRecord::Base
+    extend Ci::Model
+    include HasVariable
+    include Presentable
+
+    belongs_to :group
+
+    validates :key, uniqueness: { scope: :group_id }
+
+    scope :unprotected, -> { where(protected: false) }
+  end
+end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 0b8d0ff881a2b252f4bdc888153fde376431b74e..cf0fe04ddafe643bda2f410b1232cadf9acd7bba 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,6 +2,7 @@ module Ci
   class Variable < ActiveRecord::Base
     extend Ci::Model
     include HasVariable
+    include Presentable
 
     belongs_to :project
 
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index c28974a3cdfed941bb940f23743ad45e67e02dd1..67ecf470f7e62d973d3b2b9b0735bee71c9da6dd 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -3,6 +3,8 @@ module ShaAttribute
 
   module ClassMethods
     def sha_attribute(name)
+      return unless table_exists?
+
       column = columns.find { |c| c.name == name.to_s }
 
       # In case the table doesn't exist we won't be able to find the column,
diff --git a/app/models/group.rb b/app/models/group.rb
index b93fce6100d1fc7530adab14a850dcb8fb201465..f29e642ac91018e182adfe3594cf5fbf63075c7c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -22,6 +22,7 @@ class Group < Namespace
   has_many :shared_projects, through: :project_group_links, source: :project
   has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
   has_many :labels, class_name: 'GroupLabel'
+  has_many :variables, class_name: 'Ci::GroupVariable'
 
   validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
   validate :visibility_level_allowed_by_projects
@@ -248,6 +249,14 @@ class Group < Namespace
     }
   end
 
+  def secret_variables_for(ref, project)
+    list_of_ids = [self] + ancestors
+    variables = Ci::GroupVariable.where(group: list_of_ids)
+    variables = variables.unprotected unless project.protected_for?(ref)
+    variables = variables.group_by(&:group_id)
+    list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
+  end
+
   protected
 
   def update_two_factor_requirement
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index beaadbbd1ab118e8cdad1bc21cc2ffe5c129870b..dfca0031af851a278c82e550fe9995461bc42fa9 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -63,6 +63,10 @@ class ProjectWiki
     !!repository.exists?
   end
 
+  def has_home_page?
+    !!find_page('home')
+  end
+
   # Returns an Array of Gitlab WikiPage instances or an
   # empty Array if this Wiki has no pages.
   def pages
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index dcb37416ca3756e799c11d0a3c860d01840e7a68..6defab75fce4bae0b51ec0fcd34ddefebb9857ca 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -31,6 +31,8 @@ class GroupPolicy < BasePolicy
   rule { master }.policy do
     enable :create_projects
     enable :admin_milestones
+    enable :admin_pipeline
+    enable :admin_build
   end
 
   rule { owner }.policy do
diff --git a/app/presenters/ci/group_variable_presenter.rb b/app/presenters/ci/group_variable_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..81fea106a5c8194a7b0e8eada3d955c25dc36407
--- /dev/null
+++ b/app/presenters/ci/group_variable_presenter.rb
@@ -0,0 +1,25 @@
+module Ci
+  class GroupVariablePresenter < Gitlab::View::Presenter::Delegated
+    presents :variable
+
+    def placeholder
+      'GROUP_VARIABLE'
+    end
+
+    def form_path
+      if variable.persisted?
+        group_variable_path(group, variable)
+      else
+        group_variables_path(group)
+      end
+    end
+
+    def edit_path
+      group_variable_path(group, variable)
+    end
+
+    def delete_path
+      group_variable_path(group, variable)
+    end
+  end
+end
diff --git a/app/presenters/ci/variable_presenter.rb b/app/presenters/ci/variable_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5d7998393a6c1abc6a125d574f6866e0384725a5
--- /dev/null
+++ b/app/presenters/ci/variable_presenter.rb
@@ -0,0 +1,25 @@
+module Ci
+  class VariablePresenter < Gitlab::View::Presenter::Delegated
+    presents :variable
+
+    def placeholder
+      'PROJECT_VARIABLE'
+    end
+
+    def form_path
+      if variable.persisted?
+        project_variable_path(project, variable)
+      else
+        project_variables_path(project)
+      end
+    end
+
+    def edit_path
+      project_variable_path(project, variable)
+    end
+
+    def delete_path
+      project_variable_path(project, variable)
+    end
+  end
+end
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
similarity index 100%
rename from app/views/projects/variables/_content.html.haml
rename to app/views/ci/variables/_content.html.haml
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/ci/variables/_form.html.haml
similarity index 68%
rename from app/views/projects/variables/_form.html.haml
rename to app/views/ci/variables/_form.html.haml
index 0a70a301cb4ec3986f73b0f220a210866e9a51f6..eebd0955c801061d974cc38d95bc3374c4a05656 100644
--- a/app/views/projects/variables/_form.html.haml
+++ b/app/views/ci/variables/_form.html.haml
@@ -1,12 +1,12 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f|
+= form_for @variable, as: :variable, url: @variable.form_path do |f|
   = form_errors(@variable)
 
   .form-group
     = f.label :key, "Key", class: "label-light"
-    = f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true
+    = f.text_field :key, class: "form-control", placeholder: @variable.placeholder, required: true
   .form-group
     = f.label :value, "Value", class: "label-light"
-    = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
+    = f.text_area :value, class: "form-control", placeholder: @variable.placeholder
   .form-group
     .checkbox
       = f.label :protected do
diff --git a/app/views/projects/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
similarity index 60%
rename from app/views/projects/variables/_index.html.haml
rename to app/views/ci/variables/_index.html.haml
index 5e6786f66985502d58d779d156acff3bd472f47b..007c2344b5acf673d4373b534df80ebdac5b9436 100644
--- a/app/views/projects/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -1,16 +1,16 @@
 .row.prepend-top-default.append-bottom-default
   .col-lg-4
-    = render "projects/variables/content"
+    = render "ci/variables/content"
   .col-lg-8
     %h5.prepend-top-0
       Add a variable
-    = render "projects/variables/form", btn_text: "Add new variable"
+    = render "ci/variables/form", btn_text: "Add new variable"
     %hr
     %h5.prepend-top-0
-      Your variables (#{@project.variables.size})
-    - if @project.variables.empty?
+      Your variables (#{@variables.size})
+    - if @variables.empty?
       %p.settings-message.text-center.append-bottom-0
         No variables found, add one with the form above.
     - else
-      = render "projects/variables/table"
+      = render "ci/variables/table"
       %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/ci/variables/_show.html.haml b/app/views/ci/variables/_show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..2bfb290629d9d64ce71d4fcfe4805bac412c4f0b
--- /dev/null
+++ b/app/views/ci/variables/_show.html.haml
@@ -0,0 +1,9 @@
+- page_title "Variables"
+
+.row.prepend-top-default.append-bottom-default
+  .col-lg-3
+    = render "ci/variables/content"
+  .col-lg-9
+    %h5.prepend-top-0
+      Update variable
+    = render "ci/variables/form", btn_text: "Save variable"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml
similarity index 65%
rename from app/views/projects/variables/_table.html.haml
rename to app/views/ci/variables/_table.html.haml
index 4ce6a82881218cd0929d75a2a676ea7ab46fcf83..71a0b56c4f4cf612811d880b46bc84630070bbae 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/ci/variables/_table.html.haml
@@ -11,18 +11,18 @@
       %th Protected
       %th
     %tbody
-      - @project.variables.order_key_asc.each do |variable|
+      - @variables.each do |variable|
         - if variable.id?
           %tr
             %td.variable-key= variable.key
             %td.variable-value{ "data-value" => variable.value }******
             %td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
             %td.variable-menu
-              = link_to project_variable_path(@project, variable), class: "btn btn-transparent btn-variable-edit" do
+              = link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do
                 %span.sr-only
                   Update
                 = icon("pencil")
-              = link_to project_variable_path(@project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
+              = link_to variable.delete_path, class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do
                 %span.sr-only
                   Remove
                 = icon("trash")
diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml
index 2454e7355a71d7218827703b3ab3c5803658f3f3..623d233a46a0b03edbb41ade80f5f4bf6abcff50 100644
--- a/app/views/groups/_settings_head.html.haml
+++ b/app/views/groups/_settings_head.html.haml
@@ -12,3 +12,8 @@
           = link_to projects_group_path(@group), title: 'Projects' do
             %span
               Projects
+
+        = nav_link(controller: :ci_cd) do
+          = link_to group_settings_ci_cd_path(@group), title: 'Pipelines' do
+            %span
+              Pipelines
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..bf36baf48abddf8ebcf60439bb5bdb3bb3370b99
--- /dev/null
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -0,0 +1,4 @@
+- page_title "Pipelines"
+= render "groups/settings_head"
+
+= render 'ci/variables/index'
diff --git a/app/views/groups/variables/show.html.haml b/app/views/groups/variables/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..df533952b76ac2624929392f6bdab78347ee0679
--- /dev/null
+++ b/app/views/groups/variables/show.html.haml
@@ -0,0 +1 @@
+= render 'ci/variables/show'
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index 507f44d47452d7fbeb0a2ad96f6906d062359a4f..d8492abc638b34d4e2e3a8c09c06b68e8f4d072d 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
 = icon('info-circle fw')
 = succeed '.' do
   To learn more about this project, read
-  = link_to "the wiki", project_wikis_path(viewer.project)
+  = link_to "the wiki", get_project_wiki_path(viewer.project)
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 00ccc3ec41ef455bfccc93cd46f93a165ffc91ce..6afb38c5709b1ea9b2fd9fbc35a52026adc8cac1 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -3,6 +3,6 @@
 = render "projects/settings/head"
 
 = render 'projects/runners/index'
-= render 'projects/variables/index'
+= render 'ci/variables/index'
 = render 'projects/triggers/index'
 = render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index 297a53ca98c679861d38e5ccae109b46d46818e0..df533952b76ac2624929392f6bdab78347ee0679 100644
--- a/app/views/projects/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -1,9 +1 @@
-- page_title "Variables"
-
-.row.prepend-top-default.append-bottom-default
-  .col-lg-3
-    = render "content"
-  .col-lg-9
-    %h5.prepend-top-0
-      Update variable
-    = render "form", btn_text: "Save variable"
+= render 'ci/variables/show'
diff --git a/changelogs/unreleased/dm-readme-auxiliary-blob-viewer-without-wiki.yml b/changelogs/unreleased/dm-readme-auxiliary-blob-viewer-without-wiki.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b026a4c28993ff96f4e72d7533b4be87fe1d956
--- /dev/null
+++ b/changelogs/unreleased/dm-readme-auxiliary-blob-viewer-without-wiki.yml
@@ -0,0 +1,4 @@
+---
+title: Don't show auxiliary blob viewer for README when there is no wiki
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-intermediate-12729-group-secret-variables.yml b/changelogs/unreleased/feature-intermediate-12729-group-secret-variables.yml
new file mode 100644
index 0000000000000000000000000000000000000000..333895ffba9f64bf1e64f488c6a27c80658c4333
--- /dev/null
+++ b/changelogs/unreleased/feature-intermediate-12729-group-secret-variables.yml
@@ -0,0 +1,4 @@
+---
+title: Add Group secret variables
+merge_request: 12582
+author:
diff --git a/changelogs/unreleased/gitaly-mandatory.yml b/changelogs/unreleased/gitaly-mandatory.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c060e0add296d60fa30d9d4f62624cacedc89df1
--- /dev/null
+++ b/changelogs/unreleased/gitaly-mandatory.yml
@@ -0,0 +1,4 @@
+---
+title: Remove option to disable Gitaly
+merge_request: 12677
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 1eb209ac2be083477244d08cc17a2701294cced3..75d03de18a1b3ee9a351f665d51afcde2687f6ba 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -450,10 +450,6 @@ production: &base
 
   # Gitaly settings
   gitaly:
-    # This setting controls whether GitLab uses Gitaly (new component
-    # introduced in 9.0). Eventually Gitaly use will become mandatory and
-    # this option will disappear.
-    enabled: true
     # Default Gitaly authentication token. Can be overriden per storage. Can
     # be left blank when Gitaly is running locally on a Unix socket, which
     # is the normal way to deploy Gitaly.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index cb11d2c34f4877cf9b2b76c51806f5cd0374d68f..fa33e602e936021423d85ce0ecee174605d024c1 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -483,7 +483,6 @@ Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
 # Gitaly
 #
 Settings['gitaly'] ||= Settingslogic.new({})
-Settings.gitaly['enabled'] = true if Settings.gitaly['enabled'].nil?
 
 #
 # Webpack settings
diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb
index 31c7c91d78fd313f12c4d78943383dd32d29c1e7..f4f116e67f7599cb7800e9d3566ae7e73bc3411c 100644
--- a/config/initializers/8_gitaly.rb
+++ b/config/initializers/8_gitaly.rb
@@ -1,8 +1,6 @@
 require 'uri'
 
-if Gitlab.config.gitaly.enabled || Rails.env.test?
-  Gitlab.config.repositories.storages.keys.each do |storage|
-    # Force validation of each address
-    Gitlab::GitalyClient.address(storage)
-  end
+Gitlab.config.repositories.storages.keys.each do |storage|
+  # Force validation of each address
+  Gitlab::GitalyClient.address(storage)
 end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 11cdff55ed89d9ddafb9bed5a0f7e4839527b2d3..e578dd8b08274b4178a3bf0c5e3b877490b4a2cd 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -23,6 +23,14 @@ scope(path: 'groups/*group_id',
   resources :labels, except: [:show] do
     post :toggle_subscription, on: :member
   end
+
+  scope path: '-' do
+    namespace :settings do
+      resource :ci_cd, only: [:show], controller: 'ci_cd'
+    end
+
+    resources :variables, only: [:index, :show, :update, :create, :destroy]
+  end
 end
 
 scope(path: 'groups/*id',
diff --git a/db/migrate/20170525130346_create_group_variables_table.rb b/db/migrate/20170525130346_create_group_variables_table.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eaa38dbc40d466a3a136e9cd8f116d9238bb54b5
--- /dev/null
+++ b/db/migrate/20170525130346_create_group_variables_table.rb
@@ -0,0 +1,23 @@
+class CreateGroupVariablesTable < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def up
+    create_table :ci_group_variables do |t|
+      t.string :key, null: false
+      t.text :value
+      t.text :encrypted_value
+      t.string :encrypted_value_salt
+      t.string :encrypted_value_iv
+      t.integer :group_id, null: false
+      t.boolean :protected, default: false, null: false
+
+      t.timestamps_with_timezone null: false
+    end
+
+    add_index :ci_group_variables, [:group_id, :key], unique: true
+  end
+
+  def down
+    drop_table :ci_group_variables
+  end
+end
diff --git a/db/migrate/20170525130758_add_foreign_key_to_group_variables.rb b/db/migrate/20170525130758_add_foreign_key_to_group_variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0146235c5baa920bac304b2163f89e4b587042a3
--- /dev/null
+++ b/db/migrate/20170525130758_add_foreign_key_to_group_variables.rb
@@ -0,0 +1,15 @@
+class AddForeignKeyToGroupVariables < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_foreign_key :ci_group_variables, :namespaces, column: :group_id
+  end
+
+  def down
+    remove_foreign_key :ci_group_variables, column: :group_id
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c394ae1b9e94563fa0a71f9d87d09484514d58bd..9ed478d40c40fafd6e7abc8979f00ec07abc27f4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -266,6 +266,20 @@ ActiveRecord::Schema.define(version: 20170703102400) do
 
   add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
 
+  create_table "ci_group_variables", force: :cascade do |t|
+    t.string "key", null: false
+    t.text "value"
+    t.text "encrypted_value"
+    t.string "encrypted_value_salt"
+    t.string "encrypted_value_iv"
+    t.integer "group_id", null: false
+    t.boolean "protected", default: false, null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
+
+  add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
+
   create_table "ci_pipeline_schedules", force: :cascade do |t|
     t.string "description"
     t.string "ref"
@@ -1564,6 +1578,7 @@ ActiveRecord::Schema.define(version: 20170703102400) do
   add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
   add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
   add_foreign_key "ci_pipeline_schedule_variables", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_41c35fda51", on_delete: :cascade
+  add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
   add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade
   add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify
   add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
diff --git a/doc/README.md b/doc/README.md
index fa755852304d77643d232d6f5470184bffdda24f..ebf1a0415d208af8b58f4a8cbcb27b05e2019fdc 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -179,6 +179,7 @@ have access to GitLab administration tools and settings.
 
 ### Admin tools
 
+- [Gitaly](administration/gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service
 - [Raketasks](raketasks/README.md): Backups, maintenance, automatic webhook setup and the importing of projects.
     - [Backup and restore](raketasks/backup_restore.md): Backup and restore your GitLab instance.
 - [Reply by email](administration/reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails.
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 332457cb3841a5b2b31e5461e4103aec613b8091..5732b6a1ca453a5b1c79a98941f47ff7a044ef3e 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -2,8 +2,7 @@
 
 [Gitaly](https://gitlab.com/gitlab-org/gitaly) (introduced in GitLab
 9.0) is a service that provides high-level RPC access to Git
-repositories. As of GitLab 9.3 it is still an optional component with
-limited scope.
+repositories. Gitaly is a mandatory component in GitLab 9.4 and newer.
 
 GitLab components that access Git repositories (gitlab-rails,
 gitlab-shell, gitlab-workhorse) act as clients to Gitaly. End users do
@@ -149,6 +148,8 @@ git_data_dirs({
   { 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' } },
   { 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' } },
 })
+
+gitlab_rails['gitaly_token'] = 'abc123secret'
 ```
 
 Source installations:
@@ -164,6 +165,9 @@ gitlab:
       storage1:
         path: /mnt/gitlab/storage1/repositories
         gitaly_address: tcp://gitlab.internal:9999
+
+  gitaly:
+    token: 'abc123secret'
 ```
 
 Now reconfigure (Omnibus) or restart (source). When you tail the
@@ -172,36 +176,11 @@ Gitaly logs on your Gitaly server (`sudo gitlab-ctl tail gitaly` or
 coming in. One sure way to trigger a Gitaly request is to clone a
 repository from your GitLab server over HTTP.
 
-## Configuring GitLab to not use Gitaly
-
-Gitaly is still an optional component in GitLab 9.3. This means you
-can choose to not use it.
-
-In Omnibus you can make the following change in
-`/etc/gitlab/gitlab.rb` and reconfigure. This will both disable the
-Gitaly service and configure the rest of GitLab not to use it.
-
-```ruby
-gitaly['enable'] = false
-```
-
-In source installations, edit `/home/git/gitlab/config/gitlab.yml` and
-make sure `enabled` in the `gitaly` section is set to 'false'. This
-does not disable the Gitaly service in your init script; it only
-prevents it from being used.
-
-Apply the change with `service gitlab restart`.
-
-```yaml
-  gitaly:
-    enabled: false
-```
-
 ## Disabling or enabling the Gitaly service
 
-Be careful: if you disable Gitaly without instructing the rest of your
-GitLab installation not to use Gitaly, you may end up with errors
-because GitLab tries to access a service that is not running.
+If you are running Gitaly [as a remote
+service](#running-gitaly-on-its-own-server) you may want to disable
+the local Gitaly service that runs on your Gitlab server by default.
 
 To disable the Gitaly service in your Omnibus installation, add the
 following line to `/etc/gitlab/gitlab.rb`:
@@ -220,4 +199,4 @@ following to `/etc/default/gitlab`:
 gitaly_enabled=false
 ```
 
-When you run `service gitlab restart` Gitaly will be disabled.
\ No newline at end of file
+When you run `service gitlab restart` Gitaly will be disabled.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 78a9d49bf00a68aeece89de440066ef9876113a0..22e7f6879ed7e13d20c130b6a2d9cfa324049bef 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -10,7 +10,8 @@ The variables can be overwritten and they take precedence over each other in
 this order:
 
 1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all)
-1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
+1. Project-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
+1. Group-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
 1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
 1. YAML-defined [global variables](../yaml/README.md#variables)
 1. [Deployment variables](#deployment-variables)
@@ -142,23 +143,28 @@ script:
 
 >**Notes:**
 - This feature requires GitLab Runner 0.4.0 or higher.
+- Group-level secret variables added in GitLab 9.4.
 - Be aware that secret variables are not masked, and their values can be shown
   in the job logs if explicitly asked to do so. If your project is public or
   internal, you can set the pipelines private from your project's Pipelines
   settings. Follow the discussion in issue [#13784][ce-13784] for masking the
   secret variables.
 
-GitLab CI allows you to define per-project **secret variables** that are set in
-the build environment. The secret variables are stored out of the repository
-(`.gitlab-ci.yml`) and are securely passed to GitLab Runner making them
-available in the build environment. It's the recommended method to use for
-storing things like passwords, secret keys and credentials.
+GitLab CI allows you to define per-project or per-group **secret variables**
+that are set in the build environment. The secret variables are stored out of
+the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner
+making them available in the build environment. It's the recommended method to
+use for storing things like passwords, secret keys and credentials.
 
-Secret variables can be added by going to your project's
-**Settings âž” Pipelines**, then finding the section called
-**Secret variables**.
+Project-level secret variables can be added by going to your project's
+**Settings âž” Pipelines**, then finding the section called **Secret variables**.
 
-Once you set them, they will be available for all subsequent pipelines.
+Likewise, group-level secret variables can be added by going to your group's
+**Settings âž” Pipelines**, then finding the section called **Secret variables**.
+Any variables of [subgroups] will be inherited recursively.
+
+Once you set them, they will be available for all subsequent pipelines. You can also
+[protect your variables](#protected-secret-variables).
 
 ### Protected secret variables
 
@@ -434,3 +440,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
 [shellexecutors]: https://docs.gitlab.com/runner/executors/
 [triggered]: ../triggers/README.md
 [triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
+[subgroups]: ../../user/group/subgroups/index.md
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index a712ce5a8b1f95cec6b154870e2ef3e30b8d16c7..bbb7f4a8d48783badaa085598766408900aec280 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -148,6 +148,8 @@ sudo -u git -H make
 If you have not yet set up Gitaly then follow [Gitaly section of the installation
 guide](../install/installation.md#install-gitaly).
 
+As of GitLab 9.4, Gitaly is a mandatory component of GitLab.
+
 #### Check Gitaly configuration
 
 Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index f605c06dfc31527a5ce618245ac3a488744d2ee7..197a94487ebbd29f215b710dae4d423f62cfad2d 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -70,12 +70,8 @@ module Gitlab
       params['gitaly_token'].presence || Gitlab.config.gitaly['token']
     end
 
-    def self.enabled?
-      Gitlab.config.gitaly.enabled
-    end
-
     def self.feature_enabled?(feature, status: MigrationStatus::OPT_IN)
-      return false if !enabled? || status == MigrationStatus::DISABLED
+      return false if status == MigrationStatus::DISABLED
 
       feature = Feature.get("gitaly_#{feature}")
 
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 10eb99fb46134e9658e2c848469a3e0cf3a9da01..d81f825ef9609d65cef4c586394f51f00e0a18d4 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -112,6 +112,7 @@ module Gitlab
     # this group would not be accessible through `/groups/parent/activity` since
     # this would map to the activity-page of its parent.
     GROUP_ROUTES = %w[
+      -
       activity
       analytics
       audit_events
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index f96ee69096d4018d4bb7cb14cb81d394048484b9..4aef23b6aee1596c86c7b20a373cf213cfe5518c 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -25,27 +25,25 @@ module Gitlab
           RepoPath: repo_path
         }
 
-        if Gitlab.config.gitaly.enabled
-          server = {
-            address: Gitlab::GitalyClient.address(project.repository_storage),
-            token: Gitlab::GitalyClient.token(project.repository_storage)
-          }
-          params[:Repository] = repository.gitaly_repository.to_h
-
-          feature_enabled = case action.to_s
-                            when 'git_receive_pack'
-                              Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
-                            when 'git_upload_pack'
-                              Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
-                            when 'info_refs'
-                              true
-                            else
-                              raise "Unsupported action: #{action}"
-                            end
-          if feature_enabled
-            params[:GitalyAddress] = server[:address] # This field will be deprecated
-            params[:GitalyServer] = server
-          end
+        server = {
+          address: Gitlab::GitalyClient.address(project.repository_storage),
+          token: Gitlab::GitalyClient.token(project.repository_storage)
+        }
+        params[:Repository] = repository.gitaly_repository.to_h
+
+        feature_enabled = case action.to_s
+                          when 'git_receive_pack'
+                            Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
+                          when 'git_upload_pack'
+                            Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
+                          when 'info_refs'
+                            true
+                          else
+                            raise "Unsupported action: #{action}"
+                          end
+        if feature_enabled
+          params[:GitalyAddress] = server[:address] # This field will be deprecated
+          params[:GitalyServer] = server
         end
 
         params
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2e0efb57c74428717ad9d525739e33048c2d7207
--- /dev/null
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Groups::Settings::CiCdController do
+  let(:group) { create(:group) }
+  let(:user) { create(:user) }
+
+  before do
+    group.add_master(user)
+    sign_in(user)
+  end
+
+  describe 'GET #show' do
+    it 'renders show with 200 status code' do
+      get :show, group_id: group
+
+      expect(response).to have_http_status(200)
+      expect(response).to render_template(:show)
+    end
+  end
+end
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02f2fa4604741134eecc0785add9b3761612b7b1
--- /dev/null
+++ b/spec/controllers/groups/variables_controller_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe Groups::VariablesController do
+  let(:group) { create(:group) }
+  let(:user) { create(:user) }
+
+  before do
+    sign_in(user)
+    group.add_master(user)
+  end
+
+  describe 'POST #create' do
+    context 'variable is valid' do
+      it 'shows a success flash message' do
+        post :create, group_id: group, variable: { key: "one", value: "two" }
+
+        expect(flash[:notice]).to include 'Variable was successfully created.'
+        expect(response).to redirect_to(group_settings_ci_cd_path(group))
+      end
+    end
+
+    context 'variable is invalid' do
+      it 'renders show' do
+        post :create, group_id: group, variable: { key: "..one", value: "two" }
+
+        expect(response).to render_template("groups/variables/show")
+      end
+    end
+  end
+
+  describe 'POST #update' do
+    let(:variable) { create(:ci_group_variable) }
+
+    context 'updating a variable with valid characters' do
+      before do
+        group.variables << variable
+      end
+
+      it 'shows a success flash message' do
+        post :update, group_id: group,
+                      id: variable.id, variable: { key: variable.key, value: 'two' }
+
+        expect(flash[:notice]).to include 'Variable was successfully updated.'
+        expect(response).to redirect_to(group_variables_path(group))
+      end
+
+      it 'renders the action #show if the variable key is invalid' do
+        post :update, group_id: group,
+                      id: variable.id, variable: { key: '?', value: variable.value }
+
+        expect(response).to have_http_status(200)
+        expect(response).to render_template :show
+      end
+    end
+  end
+end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index a0ecc7566537a59db820586eaab62b9f2ea09ecc..da06fcb7cfbb652465698674da38e1a06fe82823 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -15,13 +15,13 @@ describe Projects::VariablesController do
         post :create, namespace_id: project.namespace.to_param, project_id: project,
                       variable: { key: "one", value: "two" }
 
-        expect(flash[:notice]).to include 'Variables were successfully updated.'
+        expect(flash[:notice]).to include 'Variable was successfully created.'
         expect(response).to redirect_to(project_settings_ci_cd_path(project))
       end
     end
 
     context 'variable is invalid' do
-      it 'shows an alert flash message' do
+      it 'renders show' do
         post :create, namespace_id: project.namespace.to_param, project_id: project,
                       variable: { key: "..one", value: "two" }
 
@@ -35,7 +35,6 @@ describe Projects::VariablesController do
 
     context 'updating a variable with valid characters' do
       before do
-        variable.project_id = project.id
         project.variables << variable
       end
 
diff --git a/spec/factories/ci/group_variables.rb b/spec/factories/ci/group_variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..565ced9eb1a81c2ec9b3b156f0eb77ad5939839b
--- /dev/null
+++ b/spec/factories/ci/group_variables.rb
@@ -0,0 +1,12 @@
+FactoryGirl.define do
+  factory :ci_group_variable, class: Ci::GroupVariable do
+    sequence(:key) { |n| "VARIABLE_#{n}" }
+    value 'VARIABLE_VALUE'
+
+    trait(:protected) do
+      protected true
+    end
+
+    group factory: :group
+  end
+end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..37814ba623863e446dc54afa7831ff86e49357a5
--- /dev/null
+++ b/spec/features/group_variables_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+feature 'Group variables', js: true do
+  let(:user) { create(:user) }
+  let(:group) { create(:group) }
+
+  background do
+    group.add_master(user)
+    gitlab_sign_in(user)
+  end
+
+  context 'when user creates a new variable' do
+    background do
+      visit group_settings_ci_cd_path(group)
+      fill_in 'variable_key', with: 'AAA'
+      fill_in 'variable_value', with: 'AAA123'
+      find(:css, "#variable_protected").set(true)
+      click_on 'Add new variable'
+    end
+
+    scenario 'user sees the created variable' do
+      page.within('.variables-table') do
+        expect(find(".variable-key")).to have_content('AAA')
+        expect(find(".variable-value")).to have_content('******')
+        expect(find(".variable-protected")).to have_content('Yes')
+      end
+      click_on 'Reveal Values'
+      page.within('.variables-table') do
+        expect(find(".variable-value")).to have_content('AAA123')
+      end
+    end
+  end
+
+  context 'when user edits a variable' do
+    background do
+      create(:ci_group_variable, key: 'AAA', value: 'AAA123', protected: true,
+                                 group: group)
+
+      visit group_settings_ci_cd_path(group)
+
+      page.within('.variable-menu') do
+        click_on 'Update'
+      end
+
+      fill_in 'variable_key', with: 'BBB'
+      fill_in 'variable_value', with: 'BBB123'
+      find(:css, "#variable_protected").set(false)
+      click_on 'Save variable'
+    end
+
+    scenario 'user sees the updated variable' do
+      page.within('.variables-table') do
+        expect(find(".variable-key")).to have_content('BBB')
+        expect(find(".variable-value")).to have_content('******')
+        expect(find(".variable-protected")).to have_content('No')
+      end
+    end
+  end
+
+  context 'when user deletes a variable' do
+    background do
+      create(:ci_group_variable, key: 'BBB', value: 'BBB123', protected: false,
+                                 group: group)
+
+      visit group_settings_ci_cd_path(group)
+
+      page.within('.variable-menu') do
+        page.accept_alert 'Are you sure?' do
+          click_on 'Remove'
+        end
+      end
+    end
+
+    scenario 'user does not see the deleted variable' do
+      expect(page).to have_no_css('.variables-table')
+    end
+  end
+end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index 1a2dedf27eb63d9070ce9e07a94f8e6801b5924e..7acf7a089af6e6fb18be714fb29c8208a84c2ec8 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -24,7 +24,7 @@ describe 'Project variables', js: true do
     fill_in('variable_value', with: 'key value')
     click_button('Add new variable')
 
-    expect(page).to have_content('Variables were successfully updated.')
+    expect(page).to have_content('Variable was successfully created.')
     page.within('.variables-table') do
       expect(page).to have_content('key')
       expect(page).to have_content('No')
@@ -36,7 +36,7 @@ describe 'Project variables', js: true do
     fill_in('variable_value', with: '')
     click_button('Add new variable')
 
-    expect(page).to have_content('Variables were successfully updated.')
+    expect(page).to have_content('Variable was successfully created.')
     page.within('.variables-table') do
       expect(page).to have_content('new_key')
     end
@@ -48,7 +48,7 @@ describe 'Project variables', js: true do
     check('Protected')
     click_button('Add new variable')
 
-    expect(page).to have_content('Variables were successfully updated.')
+    expect(page).to have_content('Variable was successfully created.')
     page.within('.variables-table') do
       expect(page).to have_content('key')
       expect(page).to have_content('Yes')
@@ -82,7 +82,7 @@ describe 'Project variables', js: true do
 
   it 'deletes variable' do
     page.within('.variables-table') do
-      find('.btn-variable-delete').click
+      click_on 'Remove'
     end
 
     expect(page).not_to have_selector('variables-table')
@@ -90,7 +90,7 @@ describe 'Project variables', js: true do
 
   it 'edits variable' do
     page.within('.variables-table') do
-      find('.btn-variable-edit').click
+      click_on 'Update'
     end
 
     expect(page).to have_content('Update variable')
@@ -104,7 +104,7 @@ describe 'Project variables', js: true do
 
   it 'edits variable with empty value' do
     page.within('.variables-table') do
-      find('.btn-variable-edit').click
+      click_on 'Update'
     end
 
     expect(page).to have_content('Update variable')
@@ -117,7 +117,7 @@ describe 'Project variables', js: true do
 
   it 'edits variable to be protected' do
     page.within('.variables-table') do
-      find('.btn-variable-edit').click
+      click_on 'Update'
     end
 
     expect(page).to have_content('Update variable')
@@ -132,7 +132,7 @@ describe 'Project variables', js: true do
     project.variables.first.update(protected: true)
 
     page.within('.variables-table') do
-      find('.btn-variable-edit').click
+      click_on 'Update'
     end
 
     expect(page).to have_content('Update variable')
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02679dbb5446bcab1de3048be2ec26be08225189
--- /dev/null
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe BlobViewer::Readme, model: true do
+  include FakeBlobHelpers
+
+  let(:project) { create(:project, :repository) }
+  let(:blob) { fake_blob(path: 'README.md') }
+  subject { described_class.new(blob) }
+
+  describe '#render_error' do
+    context 'when there is no wiki' do
+      it 'returns :no_wiki' do
+        expect(subject.render_error).to eq(:no_wiki)
+      end
+    end
+
+    context 'when there is an external wiki' do
+      before do
+        project.has_external_wiki = true
+      end
+
+      it 'returns nil' do
+        expect(subject.render_error).to be_nil
+      end
+    end
+
+    context 'when there is a local wiki' do
+      before do
+        project.wiki_enabled = true
+      end
+
+      context 'when the wiki is empty' do
+        it 'returns :no_wiki' do
+          expect(subject.render_error).to eq(:no_wiki)
+        end
+      end
+
+      context 'when the wiki is not empty' do
+        before do
+          WikiPages::CreateService.new(project, project.owner, title: 'home', content: 'Home page').execute
+        end
+
+        it 'returns nil' do
+          expect(subject.render_error).to be_nil
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 5ee61357c3722b46238a2bcd192eb231167ff2e8..154b6759f46a33529ccdf5c9af8367140d61459a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1356,6 +1356,59 @@ describe Ci::Build, :models do
       end
     end
 
+    context 'when group secret variable is defined' do
+      let(:secret_variable) do
+        { key: 'SECRET_KEY', value: 'secret_value', public: false }
+      end
+
+      let(:group) { create(:group, :access_requestable) }
+
+      before do
+        build.project.update(group: group)
+
+        create(:ci_group_variable,
+               secret_variable.slice(:key, :value).merge(group: group))
+      end
+
+      it { is_expected.to include(secret_variable) }
+    end
+
+    context 'when group protected variable is defined' do
+      let(:protected_variable) do
+        { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+      end
+
+      let(:group) { create(:group, :access_requestable) }
+
+      before do
+        build.project.update(group: group)
+
+        create(:ci_group_variable,
+               :protected,
+               protected_variable.slice(:key, :value).merge(group: group))
+      end
+
+      context 'when the branch is protected' do
+        before do
+          create(:protected_branch, project: build.project, name: build.ref)
+        end
+
+        it { is_expected.to include(protected_variable) }
+      end
+
+      context 'when the tag is protected' do
+        before do
+          create(:protected_tag, project: build.project, name: build.ref)
+        end
+
+        it { is_expected.to include(protected_variable) }
+      end
+
+      context 'when the ref is not protected' do
+        it { is_expected.not_to include(protected_variable) }
+      end
+    end
+
     context 'when build is for triggers' do
       let(:trigger) { create(:ci_trigger, project: project) }
       let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..24b914face94bcd81b9063d9ff4f119a2875af21
--- /dev/null
+++ b/spec/models/ci/group_variable_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Ci::GroupVariable, models: true do
+  subject { build(:ci_group_variable) }
+
+  it { is_expected.to include_module(HasVariable) }
+  it { is_expected.to include_module(Presentable) }
+  it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) }
+
+  describe '.unprotected' do
+    subject { described_class.unprotected }
+
+    context 'when variable is protected' do
+      before do
+        create(:ci_group_variable, :protected)
+      end
+
+      it 'returns nothing' do
+        is_expected.to be_empty
+      end
+    end
+
+    context 'when variable is not protected' do
+      let(:variable) { create(:ci_group_variable, protected: false) }
+
+      it 'returns the variable' do
+        is_expected.to contain_exactly(variable)
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 4ffbfa6c130c0c8b0ecaa10ba79ba9c235246bb3..890ffaae49466efc3954c91ba71147fdf75eacec 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -3,10 +3,9 @@ require 'spec_helper'
 describe Ci::Variable, models: true do
   subject { build(:ci_variable) }
 
-  let(:secret_value) { 'secret' }
-
   describe 'validations' do
     it { is_expected.to include_module(HasVariable) }
+    it { is_expected.to include_module(Presentable) }
     it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) }
   end
 
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 9e37c2b20c4aa50d850728c3dd626f67a38c785c..610793ee557032f41efd1ebf3ed3cb51a480ecb3 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -13,15 +13,34 @@ describe ShaAttribute do
   end
 
   describe '#sha_attribute' do
-    it 'defines a SHA attribute for a binary column' do
-      expect(model).to receive(:attribute)
-        .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+    context' when the table exists' do
+      before do
+        allow(model).to receive(:table_exists?).and_return(true)
+      end
 
-      model.sha_attribute(:sha1)
+      it 'defines a SHA attribute for a binary column' do
+        expect(model).to receive(:attribute)
+          .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+
+        model.sha_attribute(:sha1)
+      end
+
+      it 'raises ArgumentError when the column type is not :binary' do
+        expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+      end
     end
 
-    it 'raises ArgumentError when the column type is not :binary' do
-      expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+    context' when the table does not exist' do
+      before do
+        allow(model).to receive(:table_exists?).and_return(false)
+      end
+
+      it 'does nothing' do
+        expect(model).not_to receive(:columns)
+        expect(model).not_to receive(:attribute)
+
+        model.sha_attribute(:name)
+      end
     end
   end
 end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4de1683b21c4538a9362a773042ee46524b7f43f..399020953e8e26c77d70521b00096620fef8d017 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -13,6 +13,7 @@ describe Group, models: true do
     it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
     it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
     it { is_expected.to have_many(:labels).class_name('GroupLabel') }
+    it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
     it { is_expected.to have_many(:uploads).dependent(:destroy) }
     it { is_expected.to have_one(:chat_team) }
 
@@ -418,4 +419,69 @@ describe Group, models: true do
       expect(calls).to eq 2
     end
   end
+
+  describe '#secret_variables_for' do
+    let(:project) { create(:empty_project, group: group) }
+
+    let!(:secret_variable) do
+      create(:ci_group_variable, value: 'secret', group: group)
+    end
+
+    let!(:protected_variable) do
+      create(:ci_group_variable, :protected, value: 'protected', group: group)
+    end
+
+    subject { group.secret_variables_for('ref', project) }
+
+    shared_examples 'ref is protected' do
+      it 'contains all the variables' do
+        is_expected.to contain_exactly(secret_variable, protected_variable)
+      end
+    end
+
+    context 'when the ref is not protected' do
+      before do
+        stub_application_setting(
+          default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+      end
+
+      it 'contains only the secret variables' do
+        is_expected.to contain_exactly(secret_variable)
+      end
+    end
+
+    context 'when the ref is a protected branch' do
+      before do
+        create(:protected_branch, name: 'ref', project: project)
+      end
+
+      it_behaves_like 'ref is protected'
+    end
+
+    context 'when the ref is a protected tag' do
+      before do
+        create(:protected_tag, name: 'ref', project: project)
+      end
+
+      it_behaves_like 'ref is protected'
+    end
+
+    context 'when group has children' do
+      let!(:group_child) { create(:group, parent: group) }
+      let!(:variable_child) { create(:ci_group_variable, group: group_child) }
+      let!(:group_child_3) { create(:group, parent: group_child_2) }
+      let!(:variable_child_3) { create(:ci_group_variable, group: group_child_3) }
+      let!(:group_child_2) { create(:group, parent: group_child) }
+      let!(:variable_child_2) { create(:ci_group_variable, group: group_child_2) }
+
+      it 'returns all variables belong to the group and parent groups' do
+        expected_array1 = [protected_variable, secret_variable]
+        expected_array2 = [variable_child, variable_child_2, variable_child_3]
+        got_array = group_child_3.secret_variables_for('ref', project).to_a
+
+        expect(got_array.shift(2)).to contain_exactly(*expected_array1)
+        expect(got_array).to eq(expected_array2)
+      end
+    end
+  end
 end
diff --git a/spec/presenters/ci/group_variable_presenter_spec.rb b/spec/presenters/ci/group_variable_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d404028405b293ba446a25d608bca3a87f731fb5
--- /dev/null
+++ b/spec/presenters/ci/group_variable_presenter_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Ci::GroupVariablePresenter do
+  include Gitlab::Routing.url_helpers
+
+  let(:group) { create(:group) }
+  let(:variable) { create(:ci_group_variable, group: group) }
+
+  subject(:presenter) do
+    described_class.new(variable)
+  end
+
+  it 'inherits from Gitlab::View::Presenter::Delegated' do
+    expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+  end
+
+  describe '#initialize' do
+    it 'takes a variable and optional params' do
+      expect { presenter }.not_to raise_error
+    end
+
+    it 'exposes variable' do
+      expect(presenter.variable).to eq(variable)
+    end
+
+    it 'forwards missing methods to variable' do
+      expect(presenter.key).to eq(variable.key)
+    end
+  end
+
+  describe '#placeholder' do
+    subject { described_class.new(variable).placeholder }
+
+    it { is_expected.to eq('GROUP_VARIABLE') }
+  end
+
+  describe '#form_path' do
+    context 'when variable is persisted' do
+      subject { described_class.new(variable).form_path }
+
+      it { is_expected.to eq(group_variable_path(group, variable)) }
+    end
+
+    context 'when variable is not persisted' do
+      let(:variable) { build(:ci_group_variable, group: group) }
+      subject { described_class.new(variable).form_path }
+
+      it { is_expected.to eq(group_variables_path(group)) }
+    end
+  end
+
+  describe '#edit_path' do
+    subject { described_class.new(variable).edit_path }
+
+    it { is_expected.to eq(group_variable_path(group, variable)) }
+  end
+
+  describe '#delete_path' do
+    subject { described_class.new(variable).delete_path }
+
+    it { is_expected.to eq(group_variable_path(group, variable)) }
+  end
+end
diff --git a/spec/presenters/ci/variable_presenter_spec.rb b/spec/presenters/ci/variable_presenter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e6aae7bcad9ed3968d5c342ccc439daeddc3fda
--- /dev/null
+++ b/spec/presenters/ci/variable_presenter_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Ci::VariablePresenter do
+  include Gitlab::Routing.url_helpers
+
+  let(:project) { create(:empty_project) }
+  let(:variable) { create(:ci_variable, project: project) }
+
+  subject(:presenter) do
+    described_class.new(variable)
+  end
+
+  it 'inherits from Gitlab::View::Presenter::Delegated' do
+    expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+  end
+
+  describe '#initialize' do
+    it 'takes a variable and optional params' do
+      expect { presenter }.not_to raise_error
+    end
+
+    it 'exposes variable' do
+      expect(presenter.variable).to eq(variable)
+    end
+
+    it 'forwards missing methods to variable' do
+      expect(presenter.key).to eq(variable.key)
+    end
+  end
+
+  describe '#placeholder' do
+    subject { described_class.new(variable).placeholder }
+
+    it { is_expected.to eq('PROJECT_VARIABLE') }
+  end
+
+  describe '#form_path' do
+    context 'when variable is persisted' do
+      subject { described_class.new(variable).form_path }
+
+      it { is_expected.to eq(project_variable_path(project, variable)) }
+    end
+
+    context 'when variable is not persisted' do
+      let(:variable) { build(:ci_variable, project: project) }
+      subject { described_class.new(variable).form_path }
+
+      it { is_expected.to eq(project_variables_path(project)) }
+    end
+  end
+
+  describe '#edit_path' do
+    subject { described_class.new(variable).edit_path }
+
+    it { is_expected.to eq(project_variable_path(project, variable)) }
+  end
+
+  describe '#delete_path' do
+    subject { described_class.new(variable).delete_path }
+
+    it { is_expected.to eq(project_variable_path(project, variable)) }
+  end
+end
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index 2bf159002a074784adc44d5733f8443254f6d816..89fb362cf1403773be863ecf2fc60a68ed228284 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,8 +1,6 @@
-if Gitlab::GitalyClient.enabled?
-  RSpec.configure do |config|
-    config.before(:each) do |example|
-      next if example.metadata[:skip_gitaly_mock]
-      allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
-    end
+RSpec.configure do |config|
+  config.before(:each) do |example|
+    next if example.metadata[:skip_gitaly_mock]
+    allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
   end
 end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 32546abcad4aabb45a221dce6b3a2815a112573c..0cae5620920d783afb2b61e3018c2065a04085ca 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -69,7 +69,7 @@ module TestEnv
     # Setup GitLab shell for test instance
     setup_gitlab_shell
 
-    setup_gitaly if Gitlab::GitalyClient.enabled?
+    setup_gitaly
 
     # Create repository for FactoryGirl.create(:project)
     setup_factory_repo