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 @@ +Anchor-with-border \ 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 @@ +Anchor-with-border \ 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