diff --git a/qa/.rspec b/qa/.rspec
new file mode 100644
index 0000000000000000000000000000000000000000..b83d9b7aa658619d728a107d5d9f75ae63faff69
--- /dev/null
+++ b/qa/.rspec
@@ -0,0 +1,3 @@
+--color
+--format documentation
+--require spec_helper
diff --git a/qa/Dockerfile b/qa/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..2814a7bdef011ba7cc8fe387c3355546ad858b4c
--- /dev/null
+++ b/qa/Dockerfile
@@ -0,0 +1,14 @@
+FROM ruby:2.3
+LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
+
+RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list && \
+    apt-get update && apt-get install -y --force-yes \
+      libqt5webkit5-dev qt5-qmake qt5-default build-essential xvfb git && \
+    apt-get clean
+
+WORKDIR /home/qa
+
+COPY ./ ./
+RUN bundle install
+
+ENTRYPOINT ["bin/test"]
diff --git a/qa/Gemfile b/qa/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..6bfe25ba4371b6372bb7743b47f7269fdc923c80
--- /dev/null
+++ b/qa/Gemfile
@@ -0,0 +1,7 @@
+source 'https://rubygems.org'
+
+gem 'capybara', '~> 2.12.1'
+gem 'capybara-screenshot', '~> 1.0.14'
+gem 'capybara-webkit', '~> 1.12.0'
+gem 'rake', '~> 12.0.0'
+gem 'rspec', '~> 3.5'
diff --git a/qa/README.md b/qa/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b6b5a76f1d3d8063e0055009840374fd08ee1363
--- /dev/null
+++ b/qa/README.md
@@ -0,0 +1,18 @@
+## Integration tests for GitLab
+
+This directory contains integration tests for GitLab.
+
+It is part of [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa).
+
+## What GitLab QA is?
+
+GitLab QA is an integration tests suite for GitLab.
+
+These are black-box and entirely click-driven integration tests you can run
+against any existing instance.
+
+## How does it work?
+
+1. When we release a new version of GitLab, we build a Docker images for it.
+1. Along with GitLab Docker Images we also build and publish GitLab QA images.
+1. GitLab QA project uses these images to execute integration tests.
diff --git a/qa/bin/qa b/qa/bin/qa
new file mode 100755
index 0000000000000000000000000000000000000000..cecdeac14db8fb7b7288de0e33c43533ca5e7304
--- /dev/null
+++ b/qa/bin/qa
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+require_relative '../qa'
+
+QA::Scenario
+  .const_get(ARGV.shift)
+  .perform(*ARGV)
diff --git a/qa/bin/test b/qa/bin/test
new file mode 100755
index 0000000000000000000000000000000000000000..997392ad6e4f6a08822fafc26d75a13eeaa2ce72
--- /dev/null
+++ b/qa/bin/test
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+xvfb-run bundle exec bin/qa $@
diff --git a/qa/qa.rb b/qa/qa.rb
new file mode 100644
index 0000000000000000000000000000000000000000..58cf615cc9f894dc8738755ba50e93973b102c3c
--- /dev/null
+++ b/qa/qa.rb
@@ -0,0 +1,81 @@
+$: << File.expand_path(File.dirname(__FILE__))
+
+module QA
+  ##
+  # GitLab QA runtime classes, mostly singletons.
+  #
+  module Runtime
+    autoload :Release, 'qa/runtime/release'
+    autoload :User, 'qa/runtime/user'
+    autoload :Namespace, 'qa/runtime/namespace'
+  end
+
+  ##
+  # GitLab QA Scenarios
+  #
+  module Scenario
+    ##
+    # Support files
+    #
+    autoload :Actable, 'qa/scenario/actable'
+    autoload :Template, 'qa/scenario/template'
+
+    ##
+    # Test scenario entrypoints.
+    #
+    module Test
+      autoload :Instance, 'qa/scenario/test/instance'
+    end
+
+    ##
+    # GitLab instance scenarios.
+    #
+    module Gitlab
+      module Project
+        autoload :Create, 'qa/scenario/gitlab/project/create'
+      end
+    end
+  end
+
+  ##
+  # Classes describing structure of GitLab, pages, menus etc.
+  #
+  # Needed to execute click-driven-only black-box tests.
+  #
+  module Page
+    autoload :Base, 'qa/page/base'
+
+    module Main
+      autoload :Entry, 'qa/page/main/entry'
+      autoload :Menu, 'qa/page/main/menu'
+      autoload :Groups, 'qa/page/main/groups'
+      autoload :Projects, 'qa/page/main/projects'
+    end
+
+    module Project
+      autoload :New, 'qa/page/project/new'
+      autoload :Show, 'qa/page/project/show'
+    end
+
+    module Admin
+      autoload :Menu, 'qa/page/admin/menu'
+    end
+  end
+
+  ##
+  # Classes describing operations on Git repositories.
+  #
+  module Git
+    autoload :Repository, 'qa/git/repository'
+  end
+
+  ##
+  # Classes that make it possible to execute features tests.
+  #
+  module Specs
+    autoload :Config, 'qa/specs/config'
+    autoload :Runner, 'qa/specs/runner'
+  end
+end
+
+QA::Runtime::Release.extend_autoloads!
diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6d1601dfa487faa56db91b5d61b499537d479620
--- /dev/null
+++ b/qa/qa/ce/strategy.rb
@@ -0,0 +1,15 @@
+module QA
+  module CE
+    module Strategy
+      extend self
+
+      def extend_autoloads!
+        # noop
+      end
+
+      def perform_before_hooks
+        # noop
+      end
+    end
+  end
+end
diff --git a/qa/qa/ee.rb b/qa/qa/ee.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3f4730364d51166ee1f0778940f42236884639bb
--- /dev/null
+++ b/qa/qa/ee.rb
@@ -0,0 +1,18 @@
+module QA
+  ##
+  # GitLab EE extensions
+  #
+  module EE
+    module Page
+      module Admin
+        autoload :License, 'qa/ee/page/admin/license'
+      end
+    end
+
+    module Scenario
+      module License
+        autoload :Add, 'qa/ee/scenario/license/add'
+      end
+    end
+  end
+end
diff --git a/qa/qa/ee/page/admin/license.rb b/qa/qa/ee/page/admin/license.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ca4069f1de627d07d6c4915df54a85edcbf2b4a6
--- /dev/null
+++ b/qa/qa/ee/page/admin/license.rb
@@ -0,0 +1,22 @@
+module QA
+  module EE
+    module Page
+      module Admin
+        class License < QA::Page::Base
+          def no_license?
+            page.has_content?('No GitLab Enterprise Edition ' \
+                              'license has been provided yet')
+          end
+
+          def add_new_license(key)
+            raise 'License key empty!' if key.to_s.empty?
+
+            choose 'Enter license key'
+            fill_in 'License key', with: key
+            click_button 'Upload license'
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/ee/scenario/license/add.rb b/qa/qa/ee/scenario/license/add.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a101036d61d50d066d980a631ffd4e6b74ba2e21
--- /dev/null
+++ b/qa/qa/ee/scenario/license/add.rb
@@ -0,0 +1,21 @@
+module QA
+  module EE
+    module Scenario
+      module License
+        class Add < QA::Scenario::Template
+          def perform
+            QA::Page::Main::Entry.act { sign_in_using_credentials }
+            QA::Page::Main::Menu.act { go_to_admin_area }
+            QA::Page::Admin::Menu.act { go_to_license }
+
+            EE::Page::Admin::License.act do
+              add_new_license(ENV['EE_LICENSE']) if no_license?
+            end
+
+            QA::Page::Main::Menu.act { sign_out }
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/ee/strategy.rb b/qa/qa/ee/strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9bcd129d21843084590a9a793177592954464c57
--- /dev/null
+++ b/qa/qa/ee/strategy.rb
@@ -0,0 +1,14 @@
+module QA
+  module EE
+    module Strategy
+      extend self
+      def extend_autoloads!
+        require 'qa/ee'
+      end
+
+      def perform_before_hooks
+        EE::Scenario::License::Add.perform
+      end
+    end
+  end
+end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b9e199000d6b05a666facddd6a684cc91d957a1f
--- /dev/null
+++ b/qa/qa/git/repository.rb
@@ -0,0 +1,71 @@
+require 'uri'
+
+module QA
+  module Git
+    class Repository
+      include Scenario::Actable
+
+      def self.perform(*args)
+        Dir.mktmpdir do |dir|
+          Dir.chdir(dir) { super }
+        end
+      end
+
+      def location=(address)
+        @location = address
+        @uri = URI(address)
+      end
+
+      def username=(name)
+        @username = name
+        @uri.user = name
+      end
+
+      def password=(pass)
+        @password = pass
+        @uri.password = pass
+      end
+
+      def use_default_credentials
+        self.username = Runtime::User.name
+        self.password = Runtime::User.password
+      end
+
+      def clone(opts = '')
+        `git clone #{opts} #{@uri.to_s} ./`
+      end
+
+      def shallow_clone
+        clone('--depth 1')
+      end
+
+      def configure_identity(name, email)
+        `git config user.name #{name}`
+        `git config user.email #{email}`
+      end
+
+      def commit_file(name, contents, message)
+        add_file(name, contents)
+        commit(message)
+      end
+
+      def add_file(name, contents)
+        File.write(name, contents)
+
+        `git add #{name}`
+      end
+
+      def commit(message)
+        `git commit -m "#{message}"`
+      end
+
+      def push_changes(branch = 'master')
+        `git push #{@uri.to_s} #{branch}`
+      end
+
+      def commits
+        `git log --oneline`.split("\n")
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b01a4e10f93fd183f7a9f42587382055c6f3bf12
--- /dev/null
+++ b/qa/qa/page/admin/menu.rb
@@ -0,0 +1,19 @@
+module QA
+  module Page
+    module Admin
+      class Menu < Page::Base
+        def go_to_license
+          within_middle_menu { click_link 'License' }
+        end
+
+        private
+
+        def within_middle_menu
+          page.within('.nav-control') do
+            yield
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d55326c52621191aeb5621a11256693f8a19a350
--- /dev/null
+++ b/qa/qa/page/base.rb
@@ -0,0 +1,12 @@
+module QA
+  module Page
+    class Base
+      include Capybara::DSL
+      include Scenario::Actable
+
+      def refresh
+        visit current_path
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fe80deb642987319d1dc5a8b01a6fd8c60063c82
--- /dev/null
+++ b/qa/qa/page/main/entry.rb
@@ -0,0 +1,26 @@
+module QA
+  module Page
+    module Main
+      class Entry < Page::Base
+        def initialize
+          visit('/')
+
+          # This resolves cold boot problems with login page
+          find('.application', wait: 120)
+        end
+
+        def sign_in_using_credentials
+          if page.has_content?('Change your password')
+            fill_in :user_password, with: Runtime::User.password
+            fill_in :user_password_confirmation, with: Runtime::User.password
+            click_button 'Change your password'
+          end
+
+          fill_in :user_login, with: Runtime::User.name
+          fill_in :user_password, with: Runtime::User.password
+          click_button 'Sign in'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/groups.rb b/qa/qa/page/main/groups.rb
new file mode 100644
index 0000000000000000000000000000000000000000..84597719a84b87148a7de5314682565c693fe751
--- /dev/null
+++ b/qa/qa/page/main/groups.rb
@@ -0,0 +1,20 @@
+module QA
+  module Page
+    module Main
+      class Groups < Page::Base
+        def prepare_test_namespace
+          return if page.has_content?(Runtime::Namespace.name)
+
+          click_on 'New Group'
+
+          fill_in 'group_path', with: Runtime::Namespace.name
+          fill_in 'group_description',
+                  with: "QA test run at #{Runtime::Namespace.time}"
+          choose 'Private'
+
+          click_button 'Create group'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
new file mode 100644
index 0000000000000000000000000000000000000000..45db7a92fa42cafe3a57b2bb60429f3843e93d4a
--- /dev/null
+++ b/qa/qa/page/main/menu.rb
@@ -0,0 +1,46 @@
+module QA
+  module Page
+    module Main
+      class Menu < Page::Base
+        def go_to_groups
+          within_global_menu { click_link 'Groups' }
+        end
+
+        def go_to_projects
+          within_global_menu { click_link 'Projects' }
+        end
+
+        def go_to_admin_area
+          within_user_menu { click_link 'Admin Area' }
+        end
+
+        def sign_out
+          within_user_menu do
+            find('.header-user-dropdown-toggle').click
+            click_link('Sign out')
+          end
+        end
+
+        def has_personal_area?
+          page.has_selector?('.header-user-dropdown-toggle')
+        end
+
+        private
+
+        def within_global_menu
+          find('.global-dropdown-toggle').click
+
+          page.within('.global-dropdown-menu') do
+            yield
+          end
+        end
+
+        def within_user_menu
+          page.within('.navbar-nav') do
+            yield
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/main/projects.rb b/qa/qa/page/main/projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..28d3a4240221da07fd5339cf10f9a067d49f4038
--- /dev/null
+++ b/qa/qa/page/main/projects.rb
@@ -0,0 +1,16 @@
+module QA
+  module Page
+    module Main
+      class Projects < Page::Base
+        def go_to_new_project
+          ##
+          # There are 'New Project' and 'New project' buttons on the projects
+          # page, so we can't use `click_on`.
+          #
+          button = find('a', text: /^new project$/i)
+          button.click
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b31bec27b590747e44f0db70bec6409ed369355b
--- /dev/null
+++ b/qa/qa/page/project/new.rb
@@ -0,0 +1,24 @@
+module QA
+  module Page
+    module Project
+      class New < Page::Base
+        def choose_test_namespace
+          find('#s2id_project_namespace_id').click
+          find('.select2-result-label', text: Runtime::Namespace.name).click
+        end
+
+        def choose_name(name)
+          fill_in 'project_path', with: name
+        end
+
+        def add_description(description)
+          fill_in 'project_description', with: description
+        end
+
+        def create_new_project
+          click_on 'Create project'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
new file mode 100644
index 0000000000000000000000000000000000000000..56a270d8fcc80beaf0839ec4452f961cf100cbd8
--- /dev/null
+++ b/qa/qa/page/project/show.rb
@@ -0,0 +1,23 @@
+module QA
+  module Page
+    module Project
+      class Show < Page::Base
+        def choose_repository_clone_http
+          find('#clone-dropdown').click
+
+          page.within('#clone-dropdown') do
+            find('span', text: 'HTTP').click
+          end
+        end
+
+        def repository_location
+          find('#project_clone').value
+        end
+
+        def wait_for_push
+          sleep 5
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e4910b63a14375d17f5748d901a8515c2e4a6121
--- /dev/null
+++ b/qa/qa/runtime/namespace.rb
@@ -0,0 +1,15 @@
+module QA
+  module Runtime
+    module Namespace
+      extend self
+
+      def time
+        @time ||= Time.now
+      end
+
+      def name
+        'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S')
+      end
+    end
+  end
+end
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f83a77364570f95a350f115c28726073e1f2163
--- /dev/null
+++ b/qa/qa/runtime/release.rb
@@ -0,0 +1,28 @@
+module QA
+  module Runtime
+    ##
+    # Class that is responsible for plugging CE/EE extensions in, depending on
+    # existence of EE module.
+    #
+    # We need that to reduce the probability of conflicts when merging
+    # CE to EE.
+    #
+    class Release
+      def initialize
+        require "qa/#{version.downcase}/strategy"
+      end
+
+      def version
+        @version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE
+      end
+
+      def strategy
+        QA.const_get("QA::#{version}::Strategy")
+      end
+
+      def self.method_missing(name, *args)
+        self.new.strategy.public_send(name, *args)
+      end
+    end
+  end
+end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
new file mode 100644
index 0000000000000000000000000000000000000000..12ceda015f038f43227cb558827e8c3ab65d321f
--- /dev/null
+++ b/qa/qa/runtime/user.rb
@@ -0,0 +1,15 @@
+module QA
+  module Runtime
+    module User
+      extend self
+
+      def name
+        ENV['GITLAB_USERNAME'] || 'root'
+      end
+
+      def password
+        ENV['GITLAB_PASSWORD'] || 'test1234'
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/actable.rb b/qa/qa/scenario/actable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6cdbd24780e4e6be84854c76504cffe6936e5ad3
--- /dev/null
+++ b/qa/qa/scenario/actable.rb
@@ -0,0 +1,23 @@
+module QA
+  module Scenario
+    module Actable
+      def act(*args, &block)
+        instance_exec(*args, &block)
+      end
+
+      def self.included(base)
+        base.extend(ClassMethods)
+      end
+
+      module ClassMethods
+        def perform
+          yield new if block_given?
+        end
+
+        def act(*args, &block)
+          new.act(*args, &block)
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb
new file mode 100644
index 0000000000000000000000000000000000000000..38522714e64f45513c06f4ae86887647900ba09b
--- /dev/null
+++ b/qa/qa/scenario/gitlab/project/create.rb
@@ -0,0 +1,31 @@
+require 'securerandom'
+
+module QA
+  module Scenario
+    module Gitlab
+      module Project
+        class Create < Scenario::Template
+          attr_writer :description
+
+          def name=(name)
+            @name = "#{name}-#{SecureRandom.hex(8)}"
+          end
+
+          def perform
+            Page::Main::Menu.act { go_to_groups }
+            Page::Main::Groups.act { prepare_test_namespace }
+            Page::Main::Menu.act { go_to_projects }
+            Page::Main::Projects.act { go_to_new_project }
+
+            Page::Project::New.perform do |page|
+              page.choose_test_namespace
+              page.choose_name(@name)
+              page.add_description(@description)
+              page.create_new_project
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..341998af1609f15a9fa34eac4eedc607fdea3772
--- /dev/null
+++ b/qa/qa/scenario/template.rb
@@ -0,0 +1,16 @@
+module QA
+  module Scenario
+    class Template
+      def self.perform(*args)
+        new.tap do |scenario|
+          yield scenario if block_given?
+          return scenario.perform(*args)
+        end
+      end
+
+      def perform(*_args)
+        raise NotImplementedError
+      end
+    end
+  end
+end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
new file mode 100644
index 0000000000000000000000000000000000000000..689292bc60b8cb5277d4d4bfd474edb8f1587b8d
--- /dev/null
+++ b/qa/qa/scenario/test/instance.rb
@@ -0,0 +1,26 @@
+module QA
+  module Scenario
+    module Test
+      ##
+      # Run test suite against any GitLab instance,
+      # including staging and on-premises installation.
+      #
+      class Instance < Scenario::Template
+        def perform(address, *files)
+          Specs::Config.perform do |specs|
+            specs.address = address
+          end
+
+          ##
+          # Perform before hooks, which are different for CE and EE
+          #
+          Runtime::Release.perform_before_hooks
+
+          Specs::Runner.perform do |specs|
+            specs.rspec('--tty', files.any? ? files : 'qa/specs/features')
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d72187fcd34ff750f417e51d752a0949207a1e88
--- /dev/null
+++ b/qa/qa/specs/config.rb
@@ -0,0 +1,78 @@
+require 'rspec/core'
+require 'capybara/rspec'
+require 'capybara-webkit'
+require 'capybara-screenshot/rspec'
+
+# rubocop:disable Metrics/MethodLength
+# rubocop:disable Metrics/LineLength
+
+module QA
+  module Specs
+    class Config < Scenario::Template
+      attr_writer :address
+
+      def initialize
+        @address = ENV['GITLAB_URL']
+      end
+
+      def perform
+        raise 'Please configure GitLab address!' unless @address
+
+        configure_rspec!
+        configure_capybara!
+        configure_webkit!
+      end
+
+      def configure_rspec!
+        RSpec.configure do |config|
+          config.expect_with :rspec do |expectations|
+            # This option will default to `true` in RSpec 4. It makes the `description`
+            # and `failure_message` of custom matchers include text for helper methods
+            # defined using `chain`.
+            expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+          end
+
+          config.mock_with :rspec do |mocks|
+            # Prevents you from mocking or stubbing a method that does not exist on
+            # a real object. This is generally recommended, and will default to
+            # `true` in RSpec 4.
+            mocks.verify_partial_doubles = true
+          end
+
+          # Run specs in random order to surface order dependencies.
+          config.order = :random
+          Kernel.srand config.seed
+
+          config.before(:all) do
+            page.current_window.resize_to(1200, 1800)
+          end
+
+          config.formatter = :documentation
+          config.color = true
+        end
+      end
+
+      def configure_capybara!
+        Capybara.configure do |config|
+          config.app_host = @address
+          config.default_driver = :webkit
+          config.javascript_driver = :webkit
+          config.default_max_wait_time = 4
+
+          # https://github.com/mattheworiordan/capybara-screenshot/issues/164
+          config.save_path = 'tmp'
+        end
+      end
+
+      def configure_webkit!
+        Capybara::Webkit.configure do |config|
+          config.allow_url(@address)
+          config.block_unknown_urls
+        end
+      rescue RuntimeError # rubocop:disable Lint/HandleExceptions
+        # TODO, Webkit is already configured, this make this
+        # configuration step idempotent, should be improved.
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e1ae6efa47da70dc528e7fa4afc213b18c1e7ca
--- /dev/null
+++ b/qa/qa/specs/features/login/standard_spec.rb
@@ -0,0 +1,14 @@
+module QA
+  feature 'standard root login' do
+    scenario 'user logs in using credentials' do
+      Page::Main::Entry.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::Main::Menu.perform do |menu|
+        expect(menu).to have_personal_area
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..610492b9717e79d9f1bdb981a0c43b513d3cd355
--- /dev/null
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -0,0 +1,19 @@
+module QA
+  feature 'create a new project' do
+    scenario 'user creates a new project' do
+      Page::Main::Entry.act { sign_in_using_credentials }
+
+      Scenario::Gitlab::Project::Create.perform do |project|
+        project.name = 'awesome-project'
+        project.description = 'create awesome project test'
+      end
+
+      expect(page).to have_content(
+        /Project \S?awesome-project\S+ was successfully created/
+      )
+
+      expect(page).to have_content('create awesome project test')
+      expect(page).to have_content('The repository for this project is empty')
+    end
+  end
+end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..521bd955857e8c2a14ab5a6134c9d5e82b92cada
--- /dev/null
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -0,0 +1,57 @@
+module QA
+  feature 'clone code from the repository' do
+    context 'with regular account over http' do
+      given(:location) do
+        Page::Project::Show.act do
+          choose_repository_clone_http
+          repository_location
+        end
+      end
+
+      before do
+        Page::Main::Entry.act { sign_in_using_credentials }
+
+        Scenario::Gitlab::Project::Create.perform do |scenario|
+          scenario.name = 'project-with-code'
+          scenario.description = 'project for git clone tests'
+        end
+
+        Git::Repository.perform do |repository|
+          repository.location = location
+          repository.use_default_credentials
+
+          repository.act do
+            clone
+            configure_identity('GitLab QA', 'root@gitlab.com')
+            commit_file('test.rb', 'class Test; end', 'Add Test class')
+            commit_file('README.md', '# Test', 'Add Readme')
+            push_changes
+          end
+        end
+      end
+
+      scenario 'user performs a deep clone' do
+        Git::Repository.perform do |repository|
+          repository.location = location
+          repository.use_default_credentials
+
+          repository.act { clone }
+
+          expect(repository.commits.size).to eq 2
+        end
+      end
+
+      scenario 'user performs a shallow clone' do
+        Git::Repository.perform do |repository|
+          repository.location = location
+          repository.use_default_credentials
+
+          repository.act { shallow_clone }
+
+          expect(repository.commits.size).to eq 1
+          expect(repository.commits.first).to include 'Add Readme'
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fe45d63d37c8a83822827352be755aaa29164a1
--- /dev/null
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -0,0 +1,39 @@
+module QA
+  feature 'push code to repository' do
+    context 'with regular account over http' do
+      scenario 'user pushes code to the repository' do
+        Page::Main::Entry.act { sign_in_using_credentials }
+
+        Scenario::Gitlab::Project::Create.perform do |scenario|
+          scenario.name = 'project_with_code'
+          scenario.description = 'project with repository'
+        end
+
+        Git::Repository.perform do |repository|
+          repository.location = Page::Project::Show.act do
+            choose_repository_clone_http
+            repository_location
+          end
+
+          repository.use_default_credentials
+
+          repository.act do
+            clone
+            configure_identity('GitLab QA', 'root@gitlab.com')
+            add_file('README.md', '# This is test project')
+            commit('Add README.md')
+            push_changes
+          end
+        end
+
+        Page::Project::Show.act do
+          wait_for_push
+          refresh
+        end
+
+        expect(page).to have_content('README.md')
+        expect(page).to have_content('This is test project')
+      end
+    end
+  end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83ae15d099522d9cde8c50d69fa7bbc3dd5b7f67
--- /dev/null
+++ b/qa/qa/specs/runner.rb
@@ -0,0 +1,15 @@
+require 'rspec/core'
+
+module QA
+  module Specs
+    class Runner
+      include Scenario::Actable
+
+      def rspec(*args)
+        RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+          abort if status.nonzero?
+        end
+      end
+    end
+  end
+end
diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e6b5a8dc315e66d9654aee25166dab32af61ec08
--- /dev/null
+++ b/qa/spec/runtime/release_spec.rb
@@ -0,0 +1,50 @@
+describe QA::Runtime::Release do
+  context 'when release version has extension strategy' do
+    let(:strategy) { spy('strategy') }
+
+    before do
+      stub_const('QA::CE::Strategy', strategy)
+      stub_const('QA::EE::Strategy', strategy)
+    end
+
+    describe '#version' do
+      it 'return either CE or EE version' do
+        expect(subject.version).to eq(:CE).or eq(:EE)
+      end
+    end
+
+    describe '#strategy' do
+      it 'return the strategy constant' do
+        expect(subject.strategy).to eq strategy
+      end
+    end
+
+    describe 'delegated class methods' do
+      it 'delegates all calls to strategy class' do
+        described_class.some_method(1, 2)
+
+        expect(strategy).to have_received(:some_method)
+          .with(1, 2)
+      end
+    end
+  end
+
+  context 'when release version does not have extension strategy' do
+    before do
+      allow_any_instance_of(described_class)
+        .to receive(:version).and_return('something')
+    end
+
+    describe '#strategy' do
+      it 'raises error' do
+        expect { subject.strategy }.to raise_error(LoadError)
+      end
+    end
+
+    describe 'delegated class methods' do
+      it 'raises error' do
+        expect { described_class.some_method(2, 3) }.to raise_error(LoadError)
+      end
+    end
+  end
+end
diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..422763910e49b842927000476731e7d7669403e0
--- /dev/null
+++ b/qa/spec/scenario/actable_spec.rb
@@ -0,0 +1,47 @@
+describe QA::Scenario::Actable do
+  subject do
+    Class.new do
+      include QA::Scenario::Actable
+
+      attr_accessor :something
+
+      def do_something(arg = nil)
+        "some#{arg}"
+      end
+    end
+  end
+
+  describe '.act' do
+    it 'provides means to run steps' do
+      result = subject.act { do_something }
+
+      expect(result).to eq 'some'
+    end
+
+    it 'supports passing variables' do
+      result = subject.act('thing') do |variable|
+        do_something(variable)
+      end
+
+      expect(result).to eq 'something'
+    end
+
+    it 'returns value from the last method' do
+      result = subject.act { 'test' }
+
+      expect(result).to eq 'test'
+    end
+  end
+
+  describe '.perform' do
+    it 'makes it possible to pass binding' do
+      variable = 'something'
+
+      result = subject.perform do |object|
+        object.something = variable
+      end
+
+      expect(result).to eq 'something'
+    end
+  end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c07a32346734b883388acd66fd0aff64a301d5a2
--- /dev/null
+++ b/qa/spec/spec_helper.rb
@@ -0,0 +1,19 @@
+require_relative '../qa'
+
+RSpec.configure do |config|
+  config.expect_with :rspec do |expectations|
+    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+  end
+
+  config.mock_with :rspec do |mocks|
+    mocks.verify_partial_doubles = true
+  end
+
+  config.shared_context_metadata_behavior = :apply_to_host_groups
+  config.disable_monkey_patching!
+  config.expose_dsl_globally = true
+  config.warnings = true
+  config.profile_examples = 10
+  config.order = :random
+  Kernel.srand config.seed
+end