diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 87ec0df257a71b71c7f3b768144cc8948a48b3c8..6223e7943f854bda464525d2288350a1c0097f13 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -99,7 +99,7 @@ class Projects::PipelinesController < Projects::ApplicationController
   end
 
   def stage
-    @stage = pipeline.stage(params[:stage])
+    @stage = pipeline.legacy_stage(params[:stage])
     return not_found unless @stage
 
     respond_to do |format|
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b536af672b42e690d3add294bfbde6bd49e8d9a
--- /dev/null
+++ b/app/models/ci/legacy_stage.rb
@@ -0,0 +1,64 @@
+module Ci
+  # Currently this is artificial object, constructed dynamically
+  # We should migrate this object to actual database record in the future
+  class LegacyStage
+    include StaticModel
+
+    attr_reader :pipeline, :name
+
+    delegate :project, to: :pipeline
+
+    def initialize(pipeline, name:, status: nil, warnings: nil)
+      @pipeline = pipeline
+      @name = name
+      @status = status
+      @warnings = warnings
+    end
+
+    def groups
+      @groups ||= statuses.ordered.latest
+        .sort_by(&:sortable_name).group_by(&:group_name)
+        .map do |group_name, grouped_statuses|
+          Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
+        end
+    end
+
+    def to_param
+      name
+    end
+
+    def statuses_count
+      @statuses_count ||= statuses.count
+    end
+
+    def status
+      @status ||= statuses.latest.status
+    end
+
+    def detailed_status(current_user)
+      Gitlab::Ci::Status::Stage::Factory
+        .new(self, current_user)
+        .fabricate!
+    end
+
+    def statuses
+      @statuses ||= pipeline.statuses.where(stage: name)
+    end
+
+    def builds
+      @builds ||= pipeline.builds.where(stage: name)
+    end
+
+    def success?
+      status.to_s == 'success'
+    end
+
+    def has_warnings?
+      if @warnings.is_a?(Integer)
+        @warnings > 0
+      else
+        statuses.latest.failed_but_allowed.any?
+      end
+    end
+  end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 425ca9278ebe034cacd61babf5eaf77daa546055..e28ba91bd64a414eadf13f7638feed780b32cb19 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -11,9 +11,7 @@ module Ci
     belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
     belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
 
-    has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
-    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
-
+    has_many :stages
     has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
     has_many :builds, foreign_key: :commit_id
     has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
@@ -28,6 +26,9 @@ module Ci
     has_many :manual_actions, -> { latest.manual_actions }, foreign_key: :commit_id, class_name: 'Ci::Build'
     has_many :artifacts, -> { latest.with_artifacts_not_expired }, foreign_key: :commit_id, class_name: 'Ci::Build'
 
+    has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
+    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+
     delegate :id, to: :project, prefix: true
 
     validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
@@ -162,21 +163,21 @@ module Ci
       where.not(duration: nil).sum(:duration)
     end
 
-    def stage(name)
-      stage = Ci::Stage.new(self, name: name)
-      stage unless stage.statuses_count.zero?
-    end
-
     def stages_count
       statuses.select(:stage).distinct.count
     end
 
-    def stages_name
+    def stages_names
       statuses.order(:stage_idx).distinct.
         pluck(:stage, :stage_idx).map(&:first)
     end
 
-    def stages
+    def legacy_stage(name)
+      stage = Ci::LegacyStage.new(self, name: name)
+      stage unless stage.statuses_count.zero?
+    end
+
+    def legacy_stages
       # TODO, this needs refactoring, see gitlab-ce#26481.
 
       stages_query = statuses
@@ -191,7 +192,7 @@ module Ci
         .pluck('sg.stage', status_sql, "(#{warnings_sql})")
 
       stages_with_statuses.map do |stage|
-        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
+        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
       end
     end
 
@@ -291,12 +292,14 @@ module Ci
       end
     end
 
-    def config_builds_attributes
+    def stage_seeds
       return [] unless config_processor
 
-      config_processor.
-        builds_for_ref(ref, tag?, trigger_requests.first).
-        sort_by { |build| build[:stage_idx] }
+      @stage_seeds ||= config_processor.stage_seeds(self)
+    end
+
+    def has_stage_seeds?
+      stage_seeds.any?
     end
 
     def has_warnings?
@@ -304,7 +307,7 @@ module Ci
     end
 
     def config_processor
-      return nil unless ci_yaml_file
+      return unless ci_yaml_file
       return @config_processor if defined?(@config_processor)
 
       @config_processor ||= begin
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 9bda3186c3040d5c8941005bae38cde04855f2a9..59570924c8de1997747dbcdf12349b32165a4728 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -1,64 +1,11 @@
 module Ci
-  # Currently this is artificial object, constructed dynamically
-  # We should migrate this object to actual database record in the future
-  class Stage
-    include StaticModel
+  class Stage < ActiveRecord::Base
+    extend Ci::Model
 
-    attr_reader :pipeline, :name
+    belongs_to :project
+    belongs_to :pipeline
 
-    delegate :project, to: :pipeline
-
-    def initialize(pipeline, name:, status: nil, warnings: nil)
-      @pipeline = pipeline
-      @name = name
-      @status = status
-      @warnings = warnings
-    end
-
-    def groups
-      @groups ||= statuses.ordered.latest
-        .sort_by(&:sortable_name).group_by(&:group_name)
-        .map do |group_name, grouped_statuses|
-          Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
-        end
-    end
-
-    def to_param
-      name
-    end
-
-    def statuses_count
-      @statuses_count ||= statuses.count
-    end
-
-    def status
-      @status ||= statuses.latest.status
-    end
-
-    def detailed_status(current_user)
-      Gitlab::Ci::Status::Stage::Factory
-        .new(self, current_user)
-        .fabricate!
-    end
-
-    def statuses
-      @statuses ||= pipeline.statuses.where(stage: name)
-    end
-
-    def builds
-      @builds ||= pipeline.builds.where(stage: name)
-    end
-
-    def success?
-      status.to_s == 'success'
-    end
-
-    def has_warnings?
-      if @warnings.is_a?(Integer)
-        @warnings > 0
-      else
-        statuses.latest.failed_but_allowed.any?
-      end
-    end
+    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
+    has_many :builds, foreign_key: :commit_id
   end
 end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 8b4ed49269dad129b1a1ae570a640d0a249404e8..55c16f7e1fd69d11ed2b2cff6616ed12a01659d9 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,10 +5,10 @@ class CommitStatus < ActiveRecord::Base
 
   self.table_name = 'ci_builds'
 
+  belongs_to :user
   belongs_to :project
   belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
   belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
-  belongs_to :user
 
   delegate :commit, to: :pipeline
   delegate :sha, :short_sha, to: :pipeline
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index d58572a5f87da4239decebfd6ac7db2f58c19c2c..130968a44c151bd00df06ba32c80b3fb743638ec 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -1,6 +1,6 @@
 class PipelineDetailsEntity < PipelineEntity
   expose :details do
-    expose :stages, using: StageEntity
+    expose :legacy_stages, as: :stages, using: StageEntity
     expose :artifacts, using: BuildArtifactEntity
     expose :manual_actions, using: BuildActionEntity
   end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
deleted file mode 100644
index 70fb2c5e38f740b6968a5c4e3535cbe71cad14f5..0000000000000000000000000000000000000000
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-module Ci
-  class CreatePipelineBuildsService < BaseService
-    attr_reader :pipeline
-
-    def execute(pipeline)
-      @pipeline = pipeline
-
-      new_builds.map do |build_attributes|
-        create_build(build_attributes)
-      end
-    end
-
-    delegate :project, to: :pipeline
-
-    private
-
-    def create_build(build_attributes)
-      build_attributes = build_attributes.merge(
-        pipeline: pipeline,
-        project: project,
-        ref: pipeline.ref,
-        tag: pipeline.tag,
-        user: current_user,
-        trigger_request: trigger_request
-      )
-      build = pipeline.builds.create(build_attributes)
-
-      # Create the environment before the build starts. This sets its slug and
-      # makes it available as an environment variable
-      project.environments.find_or_create_by(name: build.expanded_environment_name) if
-        build.has_environment?
-
-      build
-    end
-
-    def new_builds
-      @new_builds ||= pipeline.config_builds_attributes.
-        reject { |build| existing_build_names.include?(build[:name]) }
-    end
-
-    def existing_build_names
-      @existing_build_names ||= pipeline.builds.pluck(:name)
-    end
-
-    def trigger_request
-      return @trigger_request if defined?(@trigger_request)
-
-      @trigger_request ||= pipeline.trigger_requests.first
-    end
-  end
-end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 13baa63220d03241a2ac9db4fd74d54a3e4aeb99..bffec216819f361e9121e470285e2d00c4eb49f1 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -43,14 +43,14 @@ module Ci
         return pipeline
       end
 
-      unless pipeline.config_builds_attributes.present?
-        return error('No builds for this pipeline.')
+      unless pipeline.has_stage_seeds?
+        return error('No stages / jobs for this pipeline.')
       end
 
       Ci::Pipeline.transaction do
         update_merge_requests_head_pipeline if pipeline.save
 
-        Ci::CreatePipelineBuildsService
+        Ci::CreatePipelineStagesService
           .new(project, current_user)
           .execute(pipeline)
       end
diff --git a/app/services/ci/create_pipeline_stages_service.rb b/app/services/ci/create_pipeline_stages_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f2c175adee67d07e9375d8fb969471476c5918fc
--- /dev/null
+++ b/app/services/ci/create_pipeline_stages_service.rb
@@ -0,0 +1,20 @@
+module Ci
+  class CreatePipelineStagesService < BaseService
+    def execute(pipeline)
+      pipeline.stage_seeds.each do |seed|
+        seed.user = current_user
+
+        seed.create! do |build|
+          ##
+          # Create the environment before the build starts. This sets its slug and
+          # makes it available as an environment variable
+          #
+          if build.has_environment?
+            environment_name = build.expanded_environment_name
+            project.environments.find_or_create_by(name: environment_name)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index f51e9fd1d54bef74d6d24285167b99e0ee4fc40b..6372e5755db7326fac8bbfb308bb3241596b72b7 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -1,7 +1,7 @@
 module Ci
   class RetryBuildService < ::BaseService
     CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
-                         allow_failure stage stage_idx trigger_request
+                         allow_failure stage_id stage stage_idx trigger_request
                          yaml_variables when environment coverage_regex
                          description tag_list].freeze
 
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 0aef5822f8106369402475a5c4e0afc2bd5b8ecd..aab503102341cb48105d13418d16e50ec15f8418 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -72,8 +72,8 @@
       Pipeline
       = link_to "##{last_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, last_pipeline.id)
       = ci_label_for_status(last_pipeline.status)
-      - if last_pipeline.stages.any?
-        with #{"stage".pluralize(last_pipeline.stages.count)}
+      - if last_pipeline.stages_count.nonzero?
+        with #{"stage".pluralize(last_pipeline.stages_count)}
         .mr-widget-pipeline-graph
           = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph'
       in
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index f700b5c945544d9fd92fb0790a8eb81964629789..09d4ddc243b60c3927ad64e0fa070f2d4e6cdb05 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -111,7 +111,7 @@
           %span.stage-selection More
           = icon('chevron-down')
         %ul.dropdown-menu
-          - @build.pipeline.stages.each do |stage|
+          - @build.pipeline.legacy_stages.each do |stage|
             %li
               %a.stage-item= stage.name
 
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 01cf2cc80e594bb9d7ff81cd7793e72a80d69ed3..85550e8fd3200e4be90c7f7526160089c6aebed7 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -42,7 +42,7 @@
             %th
             %th Coverage
             %th
-        = render partial: "projects/stage/stage", collection: pipeline.stages, as: :stage
+        = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
   - if failed_builds.present?
     #js-tab-failures.build-failures.tab-pane
       - failed_builds.each_with_index do |build, index|
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index 07970ad9cba7b16cefb7a5cc53160dffd517c971..aa93572bf9437a1f27baee17954f1ef8628d73bc 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -1,5 +1,5 @@
 .stage-cell
-  - pipeline.stages.each do |stage|
+  - pipeline.legacy_stages.each do |stage|
     - if stage.status
       - detailed_status = stage.detailed_status(current_user)
       - icon_status = "#{detailed_status.icon}_borderless"
diff --git a/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml b/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1404b3423594f26a10c652bed222515539fc41a1
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-persist-pipeline-stages.yml
@@ -0,0 +1,4 @@
+---
+title: Persist pipeline stages in the database
+merge_request: 11790
+author:
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index b06474cda7fe308fbfc0dd7c2f4e4a7724774967..22af2671b181d95d2b76ef6e480a4e7cfb93fbf7 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -50,10 +50,23 @@ module Ci
       end
     end
 
+    def stage_seeds(pipeline)
+      trigger_request = pipeline.trigger_requests.first
+
+      seeds = @stages.uniq.map do |stage|
+        builds = builds_for_stage_and_ref(
+          stage, pipeline.ref, pipeline.tag?, trigger_request)
+
+        Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
+      end
+
+      seeds.compact
+    end
+
     def build_attributes(name)
       job = @jobs[name.to_sym] || {}
-      {
-        stage_idx: @stages.index(job[:stage]),
+
+      { stage_idx: @stages.index(job[:stage]),
         stage: job[:stage],
         commands: job[:commands],
         tag_list: job[:tags] || [],
@@ -71,8 +84,7 @@ module Ci
           dependencies: job[:dependencies],
           after_script: job[:after_script],
           environment: job[:environment]
-        }.compact
-      }
+        }.compact }
     end
 
     def self.validation_message(content)
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f81f9347b4d35ba763ed0bcea8076754e25f4c15
--- /dev/null
+++ b/lib/gitlab/ci/stage/seed.rb
@@ -0,0 +1,49 @@
+module Gitlab
+  module Ci
+    module Stage
+      class Seed
+        attr_reader :pipeline
+        delegate :project, to: :pipeline
+
+        def initialize(pipeline, stage, jobs)
+          @pipeline = pipeline
+          @stage = { name: stage }
+          @jobs = jobs.to_a.dup
+        end
+
+        def user=(current_user)
+          @jobs.map! do |attributes|
+            attributes.merge(user: current_user)
+          end
+        end
+
+        def stage
+          @stage.merge(project: project)
+        end
+
+        def builds
+          trigger = pipeline.trigger_requests.first
+
+          @jobs.map do |attributes|
+            attributes.merge(project: project,
+                             ref: pipeline.ref,
+                             tag: pipeline.tag,
+                             trigger_request: trigger)
+          end
+        end
+
+        def create!
+          pipeline.stages.create!(stage).tap do |stage|
+            builds_attributes = builds.map do |attributes|
+              attributes.merge(stage_id: stage.id)
+            end
+
+            pipeline.builds.create!(builds_attributes).each do |build|
+              yield build if block_given?
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
index 182a30fd74d8286c18277b9a25f0217e2decd80a..e47fb85b5ee12cd78688783b109a4a7037589dd1 100644
--- a/lib/gitlab/data_builder/pipeline.rb
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -22,7 +22,7 @@ module Gitlab
           sha: pipeline.sha,
           before_sha: pipeline.before_sha,
           status: pipeline.status,
-          stages: pipeline.stages_name,
+          stages: pipeline.stages_names,
           created_at: pipeline.created_at,
           finished_at: pipeline.finished_at,
           duration: pipeline.duration
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index d0f3cf2b514829863b8bdcad0be8e0701d675e1e..ff2b1d08c3c393957be114b184f5cf849862f404 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -38,6 +38,7 @@ project_tree:
     - notes:
       - :author
       - :events
+    - :stages
     - :statuses
   - :triggers
   - :pipeline_schedules
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 19e23a4715f865eef939739648ae10e0e3baec06..695852526cb66ee148948ce0f0e809036f886aa3 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -3,6 +3,7 @@ module Gitlab
     class RelationFactory
       OVERRIDES = { snippets: :project_snippets,
                     pipelines: 'Ci::Pipeline',
+                    stages: 'Ci::Stage',
                     statuses: 'commit_status',
                     triggers: 'Ci::Trigger',
                     pipeline_schedules: 'Ci::PipelineSchedule',
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index d37eabb3e8c8799c15a0379c0e0b07bb8a3524e9..d3c8bf9d54fe79df1fbd4a6d1ab668e8a7f4c15a 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -1,5 +1,5 @@
 FactoryGirl.define do
-  factory :ci_stage, class: Ci::Stage do
+  factory :ci_stage, class: Ci::LegacyStage do
     skip_create
 
     transient do
@@ -10,7 +10,9 @@ FactoryGirl.define do
     end
 
     initialize_with do
-      Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings)
+      Ci::LegacyStage.new(pipeline, name: name,
+                                    status: status,
+                                    warnings: warnings)
     end
   end
 end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index fe2c00bb2cad46545e481a7b2fbf331c0e40fb09..72b9cde10e7ff6fea4936c3e8df90e23b3af8dec 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -1,7 +1,8 @@
 require 'spec_helper'
 
 module Ci
-  describe GitlabCiYamlProcessor, lib: true do
+  describe GitlabCiYamlProcessor, :lib do
+    subject { described_class.new(config, path) }
     let(:path) { 'path' }
 
     describe 'our current .gitlab-ci.yml' do
@@ -82,6 +83,48 @@ module Ci
       end
     end
 
+    describe '#stage_seeds' do
+      context 'when no refs policy is specified' do
+        let(:config) do
+          YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
+                    rspec: { stage: 'test', script: 'rspec' },
+                    spinach: { stage: 'test', script: 'spinach' })
+        end
+
+        let(:pipeline) { create(:ci_empty_pipeline) }
+
+        it 'correctly fabricates a stage seeds object' do
+          seeds = subject.stage_seeds(pipeline)
+
+          expect(seeds.size).to eq 2
+          expect(seeds.first.stage[:name]).to eq 'test'
+          expect(seeds.second.stage[:name]).to eq 'deploy'
+          expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
+          expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
+          expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+        end
+      end
+
+      context 'when refs policy is specified' do
+        let(:config) do
+          YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+                    spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
+        end
+
+        let(:pipeline) do
+          create(:ci_empty_pipeline, ref: 'feature', tag: true)
+        end
+
+        it 'returns stage seeds only assigned to master to master' do
+          seeds = subject.stage_seeds(pipeline)
+
+          expect(seeds.size).to eq 1
+          expect(seeds.first.stage[:name]).to eq 'test'
+          expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+        end
+      end
+    end
+
     describe "#builds_for_ref" do
       let(:type) { 'test' }
 
diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d7e91a5a62cbe7696b7e74b824a8ec693d92a1ba
--- /dev/null
+++ b/spec/lib/gitlab/ci/stage/seed_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Stage::Seed do
+  let(:pipeline) { create(:ci_empty_pipeline) }
+
+  let(:builds) do
+    [{ name: 'rspec' }, { name: 'spinach' }]
+  end
+
+  subject do
+    described_class.new(pipeline, 'test', builds)
+  end
+
+  describe '#stage' do
+    it 'returns hash attributes of a stage' do
+      expect(subject.stage).to be_a Hash
+      expect(subject.stage).to include(:name, :project)
+    end
+  end
+
+  describe '#builds' do
+    it 'returns hash attributes of all builds' do
+      expect(subject.builds.size).to eq 2
+      expect(subject.builds).to all(include(ref: 'master'))
+      expect(subject.builds).to all(include(tag: false))
+      expect(subject.builds).to all(include(project: pipeline.project))
+      expect(subject.builds)
+        .to all(include(trigger_request: pipeline.trigger_requests.first))
+    end
+  end
+
+  describe '#user=' do
+    let(:user) { build(:user) }
+
+    it 'assignes relevant pipeline attributes' do
+      subject.user = user
+
+      expect(subject.builds).to all(include(user: user))
+    end
+  end
+
+  describe '#create!' do
+    it 'creates all stages and builds' do
+      subject.create!
+
+      expect(pipeline.reload.stages.count).to eq 1
+      expect(pipeline.reload.builds.count).to eq 2
+      expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? })
+      expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? })
+      expect(pipeline.builds).to all(satisfy { |job| job.project.present? })
+      expect(pipeline.stages)
+        .to all(satisfy { |stage| stage.pipeline.present? })
+      expect(pipeline.stages)
+        .to all(satisfy { |stage| stage.project.present? })
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 21296a3672960fbc2bc061f7d660ba61040620b9..412eb33b35b0b1a3733dec067f118c75d190e26d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -91,6 +91,7 @@ merge_request_diff:
 pipelines:
 - project
 - user
+- stages
 - statuses
 - builds
 - trigger_requests
@@ -104,9 +105,15 @@ pipelines:
 - artifacts
 - pipeline_schedule
 - merge_requests
+stages:
+- project
+- pipeline
+- statuses
+- builds
 statuses:
 - project
 - pipeline
+- stage
 - user
 - auto_canceled_by
 variables:
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index de7852aa8103915d371d9e395bd0fb877e7a87d3..50ff6ecc1e08b19d3cd2010a9579e0a5fb81cc09 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -175,6 +175,7 @@ MergeRequestDiff:
 Ci::Pipeline:
 - id
 - project_id
+- source
 - ref
 - sha
 - before_sha
@@ -192,7 +193,13 @@ Ci::Pipeline:
 - lock_version
 - auto_canceled_by_id
 - pipeline_schedule_id
-- source
+Ci::Stage:
+- id
+- name
+- project_id
+- pipeline_id
+- created_at
+- updated_at
 CommitStatus:
 - id
 - project_id
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb
similarity index 99%
rename from spec/models/ci/stage_spec.rb
rename to spec/models/ci/legacy_stage_spec.rb
index 8f6ab90898750438e95532fb387e75f8472a0587..48116c7e7012b4af2cffda7c213a5aa7b987d274 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/legacy_stage_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Ci::Stage, models: true do
+describe Ci::LegacyStage, :models do
   let(:stage) { build(:ci_stage) }
   let(:pipeline) { stage.pipeline }
   let(:stage_name) { stage.name }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ae1b01b76ab8f524a1a5cb6357329eb8bfed291f..b50c7700bd3a9095bd1a5dbd62a83054da53e55e 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -224,8 +224,19 @@ describe Ci::Pipeline, models: true do
                              status: 'success')
     end
 
-    describe '#stages' do
-      subject { pipeline.stages }
+    describe '#stage_seeds' do
+      let(:pipeline) do
+        create(:ci_pipeline, config: { rspec: { script: 'rake' } })
+      end
+
+      it 'returns preseeded stage seeds object' do
+        expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed)
+        expect(pipeline.stage_seeds.count).to eq 1
+      end
+    end
+
+    describe '#legacy_stages' do
+      subject { pipeline.legacy_stages }
 
       context 'stages list' do
         it 'returns ordered list of stages' do
@@ -274,7 +285,7 @@ describe Ci::Pipeline, models: true do
         end
 
         it 'populates stage with correct number of warnings' do
-          deploy_stage = pipeline.stages.third
+          deploy_stage = pipeline.legacy_stages.third
 
           expect(deploy_stage).not_to receive(:statuses)
           expect(deploy_stage).to have_warnings
@@ -288,22 +299,22 @@ describe Ci::Pipeline, models: true do
       end
     end
 
-    describe '#stages_name' do
+    describe '#stages_names' do
       it 'returns a valid names of stages' do
-        expect(pipeline.stages_name).to eq(%w(build test deploy))
+        expect(pipeline.stages_names).to eq(%w(build test deploy))
       end
     end
   end
 
-  describe '#stage' do
-    subject { pipeline.stage('test') }
+  describe '#legacy_stage' do
+    subject { pipeline.legacy_stage('test') }
 
     context 'with status in stage' do
       before do
         create(:commit_status, pipeline: pipeline, stage: 'test')
       end
 
-      it { expect(subject).to be_a Ci::Stage }
+      it { expect(subject).to be_a Ci::LegacyStage }
       it { expect(subject.name).to eq 'test' }
       it { expect(subject.statuses).not_to be_empty }
     end
@@ -524,6 +535,20 @@ describe Ci::Pipeline, models: true do
     end
   end
 
+  describe '#has_stage_seeds?' do
+    context 'when pipeline has stage seeds' do
+      subject { build(:ci_pipeline_with_one_job) }
+
+      it { is_expected.to have_stage_seeds }
+    end
+
+    context 'when pipeline does not have stage seeds' do
+      subject { create(:ci_pipeline_without_jobs) }
+
+      it { is_expected.not_to have_stage_seeds }
+    end
+  end
+
   describe '#has_warnings?' do
     subject { pipeline.has_warnings? }
 
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 597c3947e716f4ef2d9b0d8247031facb08b6fcb..e9c2b865b4748f0ba14608d9b81f5c5e0e768c8e 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe Ci::CreatePipelineService, services: true do
+describe Ci::CreatePipelineService, :services do
   let(:project) { create(:project, :repository) }
   let(:user) { create(:admin) }
 
@@ -30,6 +30,7 @@ describe Ci::CreatePipelineService, services: true do
       it 'creates a pipeline' do
         expect(pipeline).to be_kind_of(Ci::Pipeline)
         expect(pipeline).to be_valid
+        expect(pipeline).to be_persisted
         expect(pipeline).to be_push
         expect(pipeline).to eq(project.pipelines.last)
         expect(pipeline).to have_attributes(user: user)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 2bd5af258474a41c168954a018bead5c43bc1b1b..ef9927c59693041075d5986603a3c5b6e4cdb2f7 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -18,20 +18,31 @@ describe Ci::RetryBuildService, :services do
        updated_at started_at finished_at queued_at erased_by
        erased_at auto_canceled_by].freeze
 
-  # TODO, move stage_id accessor to CLONE_ACCESSOR in a follow-up MR.
   IGNORE_ACCESSORS =
     %i[type lock_version target_url base_tags
        commit_id deployments erased_by_id last_deployment project_id
        runner_id tag_taggings taggings tags trigger_request_id
-       user_id auto_canceled_by_id retried stage_id].freeze
+       user_id auto_canceled_by_id retried].freeze
 
   shared_examples 'build duplication' do
+    let(:stage) do
+      # TODO, we still do not have factory for new stages, we will need to
+      # switch existing factory to persist stages, instead of using LegacyStage
+      #
+      Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test')
+    end
+
     let(:build) do
       create(:ci_build, :failed, :artifacts_expired, :erased,
              :queued, :coverage, :tags, :allowed_to_fail, :on_tag,
-             :teardown_environment, :triggered, :trace,
-             description: 'some build', pipeline: pipeline,
-             auto_canceled_by: create(:ci_empty_pipeline))
+             :triggered, :trace, :teardown_environment,
+             description: 'my-job', stage: 'test',  pipeline: pipeline,
+             auto_canceled_by: create(:ci_empty_pipeline)) do |build|
+               ##
+               # TODO, workaround for FactoryGirl limitation when having both
+               # stage (text) and stage_id (integer) columns in the table.
+               build.stage_id = stage.id
+             end
     end
 
     describe 'clone accessors' do