diff --git a/app/assets/images/ci_favicons/canary/favicon_status_manual_with_auto_play.ico b/app/assets/images/ci_favicons/canary/favicon_status_manual_with_auto_play.ico
new file mode 100644
index 00000000000..d8528e5d0e4
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_manual_with_auto_play.ico differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual_with_auto_play.png b/app/assets/images/ci_favicons/favicon_status_manual_with_auto_play.png
new file mode 100644
index 00000000000..3ca612a542d
Binary files /dev/null and b/app/assets/images/ci_favicons/favicon_status_manual_with_auto_play.png differ
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3dadb95443a..c2459b3f5f2 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -22,6 +22,7 @@ module Ci
}.freeze
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
+ has_one :build_schedule, class_name: 'Ci::BuildSchedule', foreign_key: :build_id
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
@@ -184,6 +185,12 @@ module Ci
end
end
+ after_transition any => [:manual] do |build|
+ build.run_after_commit do
+ build.schedule_delayed_execution
+ end
+ end
+
before_transition any => [:failed] do |build|
next unless build.project
next if build.retries_max.zero?
@@ -229,6 +236,20 @@ module Ci
action? && (manual? || retryable?)
end
+ def autoplay?
+ manual? && options[:autoplay_in].present?
+ end
+
+ def autoplay_at
+ ChronicDuration.parse(options[:autoplay_in])&.seconds&.from_now
+ end
+
+ def schedule_delayed_execution
+ return unless autoplay?
+
+ create_build_schedule!(execute_at: autoplay_at)
+ end
+
def action?
self.when == 'manual'
end
diff --git a/app/models/ci/build_schedule.rb b/app/models/ci/build_schedule.rb
new file mode 100644
index 00000000000..7f0a34b246d
--- /dev/null
+++ b/app/models/ci/build_schedule.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildSchedule < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+ include Importable
+ include AfterCommitQueue
+
+ belongs_to :build
+
+ after_create :schedule, unless: :importing?
+
+ def execute_in
+ self.execute_at - Time.now
+ end
+
+ private
+
+ def schedule
+ run_after_commit do
+ Ci::BuildScheduleWorker.perform_at(self.execute_at, self.build_id)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index b3960cbad1a..e2700db1438 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -92,7 +92,8 @@ module HasStatus
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
scope :cancelable, -> do
- where(status: [:running, :pending, :created])
+ where("status IN ('running', 'pending', 'created') OR " \
+ "(status = 'manual' AND EXISTS (select 1 from ci_build_schedules where ci_builds.id = ci_build_schedules.build_id))")
end
end
diff --git a/app/views/shared/icons/_icon_status_manual_with_auto_play.svg b/app/views/shared/icons/_icon_status_manual_with_auto_play.svg
new file mode 100644
index 00000000000..a08c43b156f
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual_with_auto_play.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/views/shared/icons/_icon_status_manual_with_auto_play_borderless.svg b/app/views/shared/icons/_icon_status_manual_with_auto_play_borderless.svg
new file mode 100644
index 00000000000..a08c43b156f
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_manual_with_auto_play_borderless.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 1eeb972cee9..b5a492122a3 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -60,6 +60,7 @@
- pipeline_default:build_trace_sections
- pipeline_default:pipeline_metrics
- pipeline_default:pipeline_notification
+- pipeline_default:ci_build_schedule
- pipeline_hooks:build_hooks
- pipeline_hooks:pipeline_hooks
- pipeline_processing:build_finished
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 51cbbe8882e..889384d6be8 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -9,6 +9,7 @@ class BuildFinishedWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
+ build&.build_schedule&.delete
# We execute that in sync as this access the files in order to access local file, and reduce IO
BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id)
diff --git a/app/workers/ci/build_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb
new file mode 100644
index 00000000000..448fb5bf41e
--- /dev/null
+++ b/app/workers/ci/build_schedule_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildScheduleWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ def perform(build_id)
+ ::Ci::Build.preload(:build_schedule).find_by(id: build_id).try do |build|
+ break unless build.build_schedule.present?
+
+ Ci::PlayBuildService.new(build.project, build.user).execute(build)
+ end
+ end
+ end
+end
diff --git a/db/migrate/20180913102839_create_build_schedules.rb b/db/migrate/20180913102839_create_build_schedules.rb
new file mode 100644
index 00000000000..1e9d9a70b0f
--- /dev/null
+++ b/db/migrate/20180913102839_create_build_schedules.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateBuildSchedules < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ create_table :ci_build_schedules, id: :bigserial do |t|
+ t.integer :build_id, null: false
+ t.datetime :execute_at, null: false
+
+ t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
+ t.index :build_id, unique: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b3d4badaf82..581496d78ce 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -260,6 +260,13 @@ ActiveRecord::Schema.define(version: 20180924141949) do
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
+ create_table "ci_build_schedules", id: :bigserial, force: :cascade do |t|
+ t.integer "build_id", null: false
+ t.datetime "execute_at", null: false
+ end
+
+ add_index "ci_build_schedules", ["build_id"], name: "index_ci_build_schedules_on_build_id", unique: true, using: :btree
+
create_table "ci_build_trace_chunks", id: :bigserial, force: :cascade do |t|
t.integer "build_id", null: false
t.integer "chunk_index", null: false
@@ -2288,6 +2295,7 @@ ActiveRecord::Schema.define(version: 20180924141949) do
add_foreign_key "boards", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
+ add_foreign_key "ci_build_schedules", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_build_trace_chunks", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "ci_build_trace_section_names", column: "section_name_id", name: "fk_264e112c66", on_delete: :cascade
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 016a896bde5..4376eb91a73 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -10,7 +10,7 @@ module Gitlab
include Attributable
ALLOWED_KEYS = %i[tags script only except type image services
- allow_failure type stage when artifacts cache
+ allow_failure type stage when autoplay_in artifacts cache
dependencies before_script after_script variables
environment coverage retry extends].freeze
@@ -34,6 +34,14 @@ module Gitlab
validates :dependencies, array_of_strings: true
validates :extends, type: String
+
+ with_options if: :manual_action? do
+ validates :autoplay_in, duration: true, allow_nil: true
+ end
+
+ with_options unless: :manual_action? do
+ validates :autoplay_in, presence: false
+ end
end
end
@@ -84,7 +92,7 @@ module Gitlab
:artifacts, :commands, :environment, :coverage, :retry
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :retry, :extends
+ :retry, :extends, :autoplay_in
def compose!(deps = nil)
super do
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index 2b26ebb45a1..e1b40472fc5 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -5,6 +5,7 @@ module Gitlab
class Factory < Status::Factory
def self.extended_statuses
[[Status::Build::Erased,
+ Status::Build::ManualWithAutoPlay,
Status::Build::Manual,
Status::Build::Canceled,
Status::Build::Created,
diff --git a/lib/gitlab/ci/status/build/manual_with_auto_play.rb b/lib/gitlab/ci/status/build/manual_with_auto_play.rb
new file mode 100644
index 00000000000..f34f0be5d45
--- /dev/null
+++ b/lib/gitlab/ci/status/build/manual_with_auto_play.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class ManualWithAutoPlay < Status::Extended
+ ###
+ # TODO: Those are random values. We have to fix accoding to the UX review
+ ###
+
+ ###
+ # Core override
+ ###
+ def text
+ s_('CiStatusText|scheduled')
+ end
+
+ def label
+ s_('CiStatusLabel|scheduled')
+ end
+
+ def icon
+ 'timer'
+ end
+
+ def favicon
+ 'favicon_status_manual_with_auto_play'
+ end
+
+ ###
+ # Extension override
+ ###
+ def illustration
+ {
+ image: 'illustrations/canceled-job_empty.svg',
+ size: 'svg-394',
+ title: _('This job is a scheduled job with manual actions!'),
+ content: _('auto playyyyyyyyyyyyyy! This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
+ }
+ end
+
+ def status_tooltip
+ @status.status_tooltip + " (scheulded) : Execute in #{subject.build_schedule.execute_in.round} sec"
+ end
+
+ def self.matches?(build, user)
+ build.autoplay? && !build.canceled?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 5d1864ae9e2..5277b69a628 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -49,7 +49,8 @@ module Gitlab
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
- retry: job[:retry]
+ retry: job[:retry],
+ autoplay_in: job[:autoplay_in],
}.compact }
end