diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 4c82d1868..4d2a12074 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -56,6 +56,7 @@ object Dependencies {
"com.adrianhurt" %% "play-bootstrap" % "1.2-P26-B3-RC2",
"com.gu" %% "scanamo" % "0.9.5",
"com.amazonaws" % "aws-java-sdk-dynamodb" % Versions.aws,
+ "org.quartz-scheduler" % "quartz" % "2.3.0",
"org.webjars" %% "webjars-play" % "2.6.0",
"org.webjars" % "jquery" % "3.1.1",
"org.webjars" % "jquery-ui" % "1.12.1",
diff --git a/riff-raff/app/AppComponents.scala b/riff-raff/app/AppComponents.scala
index dd5a9ce04..0ed2f53cf 100644
--- a/riff-raff/app/AppComponents.scala
+++ b/riff-raff/app/AppComponents.scala
@@ -4,6 +4,7 @@ import controllers._
import deployment.preview.PreviewCoordinator
import deployment.{DeploymentEngine, Deployments}
import magenta.deployment_type._
+import persistence.ScheduleRepository
import play.api.ApplicationLoader.Context
import play.api.http.DefaultHttpErrorHandler
import play.api.i18n.I18nComponents
@@ -20,13 +21,15 @@ import utils.HstsFilter
import scala.concurrent.Future
import scala.concurrent.duration._
import router.Routes
+import schedule.DeployScheduler
class AppComponents(context: Context) extends BuiltInComponentsFromContext(context)
with AhcWSComponents
with I18nComponents
with CSRFComponents
with GzipFilterComponents
- with AssetsComponents {
+ with AssetsComponents
+ with Logging {
implicit val implicitMessagesApi = messagesApi
implicit val implicitWsClient = wsClient
@@ -52,6 +55,15 @@ class AppComponents(context: Context) extends BuiltInComponentsFromContext(conte
new HstsFilter()(executionContext)
) // TODO (this would require an upgrade of the management-play lib) ++ PlayRequestMetrics.asFilters
+ val deployScheduler = new DeployScheduler(deployments)
+ log.info("Starting deployment scheduler")
+ deployScheduler.start()
+ applicationLifecycle.addStopHook { () =>
+ log.info("Shutting down deployment scheduler")
+ Future.successful(deployScheduler.shutdown())
+ }
+ deployScheduler.initialise(ScheduleRepository.getScheduleList())
+
val applicationController = new Application(prismLookup, availableDeploymentTypes, authAction, controllerComponents, assets)(environment, wsClient, executionContext)
val deployController = new DeployController(deployments, prismLookup, availableDeploymentTypes, builds, authAction, controllerComponents)
val apiController = new Api(deployments, availableDeploymentTypes, authAction, controllerComponents)
@@ -59,6 +71,7 @@ class AppComponents(context: Context) extends BuiltInComponentsFromContext(conte
val previewController = new PreviewController(previewCoordinator, authAction, controllerComponents)(wsClient, executionContext)
val hooksController = new HooksController(prismLookup, authAction, controllerComponents)
val restrictionsController = new Restrictions(authAction, controllerComponents)
+ val scheduleController = new ScheduleController(authAction, controllerComponents, prismLookup, deployScheduler)
val targetController = new TargetController(deployments, authAction, controllerComponents)
val loginController = new Login(deployments, controllerComponents, authAction)
val testingController = new Testing(prismLookup, authAction, controllerComponents)
@@ -80,6 +93,7 @@ class AppComponents(context: Context) extends BuiltInComponentsFromContext(conte
continuousDeployController,
hooksController,
restrictionsController,
+ scheduleController,
targetController,
loginController,
testingController,
diff --git a/riff-raff/app/assets/javascripts/form-timezone.coffee b/riff-raff/app/assets/javascripts/form-timezone.coffee
new file mode 100644
index 000000000..b8cb92cc7
--- /dev/null
+++ b/riff-raff/app/assets/javascripts/form-timezone.coffee
@@ -0,0 +1,5 @@
+$ ->
+ if $('#timezoneInput').prop('selectedIndex') == 0
+ myTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
+ $('#timezoneInput').val(myTimeZone)
+
diff --git a/riff-raff/app/conf/context.scala b/riff-raff/app/conf/context.scala
index b9256098d..23afa73bf 100644
--- a/riff-raff/app/conf/context.scala
+++ b/riff-raff/app/conf/context.scala
@@ -89,7 +89,10 @@ class Configuration(val application: String, val webappConfDirectory: String = "
object continuousDeployment {
lazy val enabled = configuration.getStringProperty("continuousDeployment.enabled", "false") == "true"
+ }
+ object scheduledDeployment {
+ lazy val enabled = configuration.getStringProperty("scheduledDeployment.enabled", "false") == "true"
}
object credentials {
diff --git a/riff-raff/app/controllers/Application.scala b/riff-raff/app/controllers/Application.scala
index 3e8606cb2..0c6f21e8e 100644
--- a/riff-raff/app/controllers/Application.scala
+++ b/riff-raff/app/controllers/Application.scala
@@ -50,7 +50,8 @@ object Menu {
SingleMenuItem("Hooks", routes.HooksController.list()),
SingleMenuItem("Authorisation", routes.Login.authList(), enabled = conf.Configuration.auth.whitelist.useDatabase),
SingleMenuItem("API keys", routes.Api.listKeys()),
- SingleMenuItem("Restrictions", routes.Restrictions.list())
+ SingleMenuItem("Restrictions", routes.Restrictions.list()),
+ SingleMenuItem("Schedules", routes.ScheduleController.list())
)),
DropDownMenuItem("Documentation", Seq(
SingleMenuItem("Deployment Types", routes.Application.documentation("magenta-lib/types")),
diff --git a/riff-raff/app/controllers/ScheduleController.scala b/riff-raff/app/controllers/ScheduleController.scala
new file mode 100644
index 000000000..5f0273388
--- /dev/null
+++ b/riff-raff/app/controllers/ScheduleController.scala
@@ -0,0 +1,110 @@
+package controllers
+
+import java.text.ParseException
+import java.util.{TimeZone, UUID}
+
+import com.gu.googleauth.AuthAction
+import org.joda.time.DateTime
+import org.quartz.CronExpression
+import persistence.ScheduleRepository
+import play.api.data.Form
+import play.api.data.Forms._
+import play.api.data.validation.{Constraint, Invalid, Valid}
+import play.api.i18n.I18nSupport
+import play.api.libs.ws.WSClient
+import play.api.mvc.{AnyContent, BaseController, ControllerComponents}
+import resources.PrismLookup
+import schedule.{DeployScheduler, ScheduleConfig}
+
+import scala.util.{Failure, Success, Try}
+
+class ScheduleController(authAction: AuthAction[AnyContent], val controllerComponents: ControllerComponents,
+ prismLookup: PrismLookup, deployScheduler: DeployScheduler)(implicit val wsClient: WSClient)
+ extends BaseController with Logging with I18nSupport {
+
+ import ScheduleController.ScheduleForm
+
+ val quartzExpressionConstraint: Constraint[String] = Constraint("quartz.expression"){ expression =>
+ Try(CronExpression.validateExpression(expression)) match {
+ case Success(()) => Valid
+ case Failure(pe:ParseException) => Invalid(s"Invalid Quartz expression: ${pe.getMessage}")
+ case Failure(_) => Invalid(s"Invalid Quartz expression")
+ }
+ }
+ val timezoneConstraint: Constraint[String] = Constraint("timezone"){ expression =>
+ Try(TimeZone.getTimeZone(expression)) match {
+ case Success(_) => Valid
+ case Failure(_) => Invalid(s"Invalid timezone")
+ }
+ }
+ val timeZones: List[String] = TimeZone.getAvailableIDs.toList.sorted
+
+ val scheduleForm = Form[ScheduleForm](
+ mapping(
+ "id" -> uuid,
+ "projectName" -> nonEmptyText,
+ "stage" -> nonEmptyText,
+ "schedule" -> nonEmptyText.verifying(quartzExpressionConstraint),
+ "timezone" -> nonEmptyText.verifying(timezoneConstraint),
+ "enabled" -> boolean
+ )(ScheduleForm.apply)(ScheduleForm.unapply)
+ )
+
+ def list = authAction { implicit request =>
+ val schedules = ScheduleRepository.getScheduleList()
+ Ok(views.html.schedule.list(request, schedules))
+ }
+
+ def form = authAction { implicit request =>
+ Ok(views.html.schedule.form(
+ scheduleForm.fill(ScheduleForm(UUID.randomUUID(), "", "", "", "", enabled = true)), prismLookup, timeZones
+ ))
+ }
+
+ def save = authAction { implicit request =>
+ scheduleForm.bindFromRequest().fold(
+ formWithErrors => Ok(views.html.schedule.form(formWithErrors, prismLookup, timeZones)),
+ form => {
+ val config = form.toConfig(new DateTime(), request.user.fullName)
+ ScheduleRepository.setSchedule(config)
+ deployScheduler.reschedule(config)
+ Redirect(routes.ScheduleController.list())
+ }
+ )
+ }
+
+ def edit(id: String) = authAction { implicit request =>
+ ScheduleRepository.getSchedule(UUID.fromString(id))
+ .fold(NotFound(s"Schedule with ID $id doesn't exist"))(
+ config => Ok(views.html.schedule.form(scheduleForm.fill(ScheduleForm(config)), prismLookup, timeZones))
+ )
+ }
+
+ def delete(id: String) = authAction { implicit request =>
+ Form("action" -> nonEmptyText).bindFromRequest().fold(
+ errors => {},
+ {
+ case "delete" =>
+ val uuid = UUID.fromString(id)
+ ScheduleRepository.deleteSchedule(uuid)
+ deployScheduler.unschedule(uuid)
+ }
+ )
+ Redirect(routes.ScheduleController.list())
+ }
+
+}
+
+object ScheduleController {
+
+ case class ScheduleForm(id: UUID, projectName: String, stage: String, schedule: String, timezone: String, enabled: Boolean) {
+ def toConfig(lastEdited: DateTime, user: String): ScheduleConfig =
+ ScheduleConfig(id, projectName, stage, schedule, timezone, enabled, lastEdited, user)
+ }
+ object ScheduleForm {
+ def apply(config: ScheduleConfig): ScheduleForm =
+ ScheduleForm(config.id, config.projectName, config.stage, config.scheduleExpression, config.timezone, config.enabled)
+ }
+
+}
+
diff --git a/riff-raff/app/deployment/model.scala b/riff-raff/app/deployment/model.scala
index da84ddd87..7e95c6e89 100644
--- a/riff-raff/app/deployment/model.scala
+++ b/riff-raff/app/deployment/model.scala
@@ -14,6 +14,7 @@ sealed trait RequestSource
case class UserRequestSource(user: UserIdentity) extends RequestSource
case object ContinuousDeploymentRequestSource extends RequestSource
case class ApiRequestSource(key: ApiKey) extends RequestSource
+case object ScheduleRequestSource extends RequestSource
case class Error(message: String) extends AnyVal
diff --git a/riff-raff/app/persistence/ScheduleRepository.scala b/riff-raff/app/persistence/ScheduleRepository.scala
new file mode 100644
index 000000000..27e08a031
--- /dev/null
+++ b/riff-raff/app/persistence/ScheduleRepository.scala
@@ -0,0 +1,30 @@
+package persistence
+
+import java.util.UUID
+
+import ci.Trigger
+import com.gu.scanamo.syntax._
+import com.gu.scanamo.{DynamoFormat, Table}
+import schedule.ScheduleConfig
+
+object ScheduleRepository extends DynamoRepository {
+
+ implicit val triggerModeFormat =
+ DynamoFormat.coercedXmap[Trigger.Mode, String, NoSuchElementException](Trigger.withName)(_.toString)
+
+ override val tablePrefix = "schedule-config"
+
+ val table = Table[ScheduleConfig](tableName)
+
+ def getScheduleList(): List[ScheduleConfig] =
+ exec(table.scan()).flatMap(_.toOption)
+
+ def getSchedule(id: UUID): Option[ScheduleConfig] =
+ exec(table.get('id -> id)).flatMap(_.toOption)
+
+ def setSchedule(schedule: ScheduleConfig): Unit =
+ exec(table.put(schedule))
+
+ def deleteSchedule(id: UUID): Unit =
+ exec(table.delete('id -> id))
+}
diff --git a/riff-raff/app/schedule/DeployJob.scala b/riff-raff/app/schedule/DeployJob.scala
new file mode 100644
index 000000000..0cdbb7ca6
--- /dev/null
+++ b/riff-raff/app/schedule/DeployJob.scala
@@ -0,0 +1,73 @@
+package schedule
+
+import conf.Configuration
+import controllers.Logging
+import deployment._
+import magenta.{Deployer, DeployParameters, RunState}
+import org.quartz.{Job, JobDataMap, JobExecutionContext}
+import schedule.DeployScheduler.JobDataKeys
+
+import scala.annotation.tailrec
+import scala.util.Try
+
+class DeployJob extends Job with Logging {
+ private def getAs[T](key: String)(implicit jobDataMap: JobDataMap): T = jobDataMap.get(key).asInstanceOf[T]
+
+ override def execute(context: JobExecutionContext): Unit = {
+ implicit val jobDataMap = context.getJobDetail.getJobDataMap
+ val deployments = getAs[Deployments](JobDataKeys.Deployments)
+ val projectName = getAs[String](JobDataKeys.ProjectName)
+ val stage = getAs[String](JobDataKeys.Stage)
+
+ val result = for {
+ record <- DeployJob.getLastDeploy(deployments, projectName, stage)
+ params <- DeployJob.createDeployParameters(record, Configuration.scheduledDeployment.enabled)
+ uuid <- deployments.deploy(params, ScheduleRequestSource)
+ } yield uuid
+ result match {
+ case Left(error) => log.warn(error.message)
+ case Right(uuid) => log.info(s"Started scheduled deploy $uuid")
+ }
+ }
+}
+
+object DeployJob {
+ def createDeployParameters(lastDeploy: Record, scheduledDeploysEnabled: Boolean): Either[Error, DeployParameters] = {
+ lastDeploy.state match {
+ case RunState.Completed =>
+ val params = DeployParameters(
+ Deployer("Scheduled Deployment"),
+ lastDeploy.parameters.build,
+ lastDeploy.stage
+ )
+ if (scheduledDeploysEnabled) {
+ Right(params)
+ } else {
+ Left(Error(s"Scheduled deployments disabled. Would have deployed $params"))
+ }
+ case otherState =>
+ Left(Error(s"Skipping scheduled deploy as deploy record ${lastDeploy.uuid} has status $otherState"))
+ }
+ }
+
+ @tailrec
+ private def getLastDeploy(deployments: Deployments, projectName: String, stage: String, attempts: Int = 5): Either[Error, Record] = {
+ if (attempts == 0) {
+ Left(Error(s"Didn't find any deploys for $projectName / $stage"))
+ } else {
+ val filter = DeployFilter(
+ projectName = Some(projectName),
+ stage = Some(stage)
+ )
+ val pagination = PaginationView().withPageSize(Some(1))
+
+ val result = Try(deployments.getDeploys(Some(filter), pagination).headOption).toOption.flatten
+ result match {
+ case Some(record) => Right(record)
+ case None =>
+ Thread.sleep(1000)
+ getLastDeploy(deployments, projectName, stage, attempts-1)
+ }
+ }
+ }
+}
diff --git a/riff-raff/app/schedule/DeployScheduler.scala b/riff-raff/app/schedule/DeployScheduler.scala
new file mode 100644
index 000000000..50c43ea7c
--- /dev/null
+++ b/riff-raff/app/schedule/DeployScheduler.scala
@@ -0,0 +1,77 @@
+package schedule
+
+import java.util.{TimeZone, UUID}
+
+import deployment.Deployments
+import org.quartz.CronScheduleBuilder._
+import org.quartz.JobBuilder._
+import org.quartz.TriggerBuilder._
+import org.quartz.impl.StdSchedulerFactory
+import org.quartz.{JobDataMap, JobKey, TriggerKey}
+import play.api.Logger
+import schedule.DeployScheduler.JobDataKeys
+
+class DeployScheduler(deployments: Deployments) {
+
+ private val scheduler = StdSchedulerFactory.getDefaultScheduler
+
+ def initialise(schedules: Iterable[ScheduleConfig]): Unit = {
+ schedules.foreach(scheduleDeploy)
+ }
+
+ def reschedule(schedule: ScheduleConfig): Unit = {
+ // Delete any job and trigger that we may have previously created
+ unschedule(schedule.id)
+ scheduleDeploy(schedule)
+ }
+
+ def unschedule(id: UUID): Unit = {
+ scheduler.deleteJob(jobKey(id))
+ }
+
+ private def scheduleDeploy(scheduleConfig: ScheduleConfig): Unit = {
+ val id = scheduleConfig.id
+ if (scheduleConfig.enabled) {
+ val jobDetail = newJob(classOf[DeployJob])
+ .withIdentity(jobKey(id))
+ .usingJobData(buildJobDataMap(scheduleConfig))
+ .build()
+ val trigger = newTrigger()
+ .withIdentity(triggerKey(id))
+ .withSchedule(
+ cronSchedule(scheduleConfig.scheduleExpression).inTimeZone(TimeZone.getTimeZone(scheduleConfig.timezone))
+ )
+ .build()
+ scheduler.scheduleJob(jobDetail, trigger)
+ Logger.info(s"Scheduled [$id] to deploy with schedule [${scheduleConfig.scheduleExpression} in ${scheduleConfig.timezone}]")
+ } else {
+ Logger.info(s"NOT scheduling disabled schedule [$id] to deploy with schedule [${scheduleConfig.scheduleExpression} in ${scheduleConfig.timezone}]")
+ }
+ }
+
+ def start(): Unit = scheduler.start()
+
+ def shutdown(): Unit = scheduler.shutdown()
+
+ private def jobKey(id: UUID): JobKey = new JobKey(id.toString)
+ private def triggerKey(id: UUID): TriggerKey = new TriggerKey(id.toString)
+
+ private def buildJobDataMap(scheduleConfig: ScheduleConfig): JobDataMap = {
+ val map = new JobDataMap()
+ map.put(JobDataKeys.Deployments, deployments)
+ map.put(JobDataKeys.ProjectName, scheduleConfig.projectName)
+ map.put(JobDataKeys.Stage, scheduleConfig.stage)
+ map
+ }
+
+}
+
+object DeployScheduler {
+
+ object JobDataKeys {
+ val Deployments = "deployments"
+ val ProjectName = "projectName"
+ val Stage = "stage"
+ }
+
+}
diff --git a/riff-raff/app/schedule/ScheduleConfig.scala b/riff-raff/app/schedule/ScheduleConfig.scala
new file mode 100644
index 000000000..509f34571
--- /dev/null
+++ b/riff-raff/app/schedule/ScheduleConfig.scala
@@ -0,0 +1,9 @@
+package schedule
+
+import java.util.UUID
+
+import org.joda.time.DateTime
+
+case class ScheduleConfig(id: UUID, projectName: String, stage: String,
+ scheduleExpression: String, timezone: String,
+ enabled: Boolean, lastEdited: DateTime, user: String)
\ No newline at end of file
diff --git a/riff-raff/app/views/schedule/form.scala.html b/riff-raff/app/views/schedule/form.scala.html
new file mode 100644
index 000000000..f4f6ec0de
--- /dev/null
+++ b/riff-raff/app/views/schedule/form.scala.html
@@ -0,0 +1,53 @@
+@(configForm: Form[controllers.ScheduleController.ScheduleForm], prismLookup: resources.PrismLookup, timeZones: List[String])(implicit request: Security.AuthenticatedRequest[AnyContent, com.gu.googleauth.UserIdentity], messages: Messages)
+@import b3.vertical.fieldConstructor
+@import helper.CSRF
+
+@main("Schedule", request, List("form-autocomplete", "form-timezone")) {
+
+
Schedule Configuration
+
+
+ @b3.form(action=routes.ScheduleController.save) {
+ @CSRF.formField
+ @snippets.inputHidden(configForm("id"))
+
+ @if(configForm.hasGlobalErrors) {
+
+
Error
+
+ @configForm.globalErrors.map { error =>
+ - @error.message
+ }
+
+
+ }
+
+ @b3.text(configForm("projectName"), 'id -> "projectInput", Symbol("data-url") -> "/deployment/request/autoComplete/project", '_label -> "Project Name")
+ @b3.select(
+ configForm("stage"),
+ options = helper.options(prismLookup.stages.toList),
+ '_default -> "--- Choose a stage ---",
+ '_label -> "Stage",
+ '_error -> configForm.globalError.map(_.withMessage("Please select deployment stage"))
+ )
+ @b3.text(configForm("schedule"), '_label -> "Schedule expression")
+ This should be a weird Quartz cron expression
+ (docs)
+ (online evaluator).
+ For example 0 30 11 ? * TUE *
is 11:30 every Tuesday.
+ @b3.select(
+ configForm("timezone"),
+ options = helper.options(timeZones),
+ 'id -> "timezoneInput",
+ '_default -> "--- Choose a timezone ---",
+ '_label -> "Schedule timezone",
+ '_error -> configForm.globalError.map(_.withMessage("Please select a valid timezone for the schedule"))
+ )
+ @b3.checkbox(configForm("enabled"), '_label -> "Schedule Enabled")
+
+
+ }
+}
\ No newline at end of file
diff --git a/riff-raff/app/views/schedule/list.scala.html b/riff-raff/app/views/schedule/list.scala.html
new file mode 100644
index 000000000..e4aa77715
--- /dev/null
+++ b/riff-raff/app/views/schedule/list.scala.html
@@ -0,0 +1,51 @@
+@import _root_.schedule.ScheduleConfig
+@(implicit request: Security.AuthenticatedRequest[AnyContent, com.gu.googleauth.UserIdentity], configs: Seq[ScheduleConfig])
+
+@import org.joda.time.format.DateTimeFormat._
+@import ci.Trigger._
+@import helper.CSRF
+
+ @main("Schedule Configurations", request) {
+ Schedule Configurations
+
+ Add new schedule
+
+ @if(configs.isEmpty) {
+
No configurations.
+ } else {
+
+
+
+ Last edited |
+ Project Name |
+ Target Stage |
+ Schedule |
+ Enabled? |
+ |
+ |
+
+
+
+ @for(config <- configs) {
+
+ @utils.DateFormats.Short.print(config.lastEdited) by @config.user |
+ @config.projectName |
+ @config.stage |
+ @config.scheduleExpression (@config.timezone) |
+ @config.enabled |
+
+ Edit
+ |
+
+ @helper.form(action=routes.ScheduleController.delete(config.id.toString), 'class -> "form-make-inline") {
+ @CSRF.formField
+
+ }
+ |
+
+ }
+
+
+ }
+
+ }
diff --git a/riff-raff/conf/routes b/riff-raff/conf/routes
index 6a915e18b..073e0536f 100644
--- a/riff-raff/conf/routes
+++ b/riff-raff/conf/routes
@@ -75,6 +75,13 @@ POST /deployment/restrictions/save controllers.Restrict
GET /deployment/restrictions/edit controllers.Restrictions.edit(id)
POST /deployment/restrictions/delete controllers.Restrictions.delete(id)
+# Schedules
+GET /deployment/schedule controllers.ScheduleController.list
+GET /deployment/schedule/new controllers.ScheduleController.form
+POST /deployment/schedule/save controllers.ScheduleController.save
+GET /deployment/schedule/:id/edit controllers.ScheduleController.edit(id)
+POST /deployment/schedule/:id/delete controllers.ScheduleController.delete(id)
+
# Target
GET /deployment/target/find controllers.TargetController.findMatch(region: String, stack: String, app: String)
GET /deployment/target/deploy controllers.TargetController.findAppropriateDeploy(region: String, stack: String, app: String, stage: String)
diff --git a/riff-raff/test/schedule/DeployJobTest.scala b/riff-raff/test/schedule/DeployJobTest.scala
new file mode 100644
index 000000000..78cca8214
--- /dev/null
+++ b/riff-raff/test/schedule/DeployJobTest.scala
@@ -0,0 +1,44 @@
+package schedule
+
+import java.util.UUID
+
+import deployment.{DeployRecord, Error}
+import magenta.{Build, Deployer, DeployParameters, RunState, Stage}
+import org.joda.time.DateTime
+import org.scalatest.{EitherValues, FlatSpec, Matchers}
+
+class DeployJobTest extends FlatSpec with Matchers with EitherValues {
+ val uuid = UUID.fromString("7fa2ee0a-8d90-4f7e-a38b-185f36fbc5aa")
+ "createDeployParameters" should "return params if valid" in {
+ val record = new DeployRecord(
+ new DateTime(),
+ uuid,
+ DeployParameters(Deployer("Bob"), Build("testProject", "1"), Stage("TEST")),
+ recordState = Some(RunState.Completed)
+ )
+ DeployJob.createDeployParameters(record, true) shouldBe
+ Right(DeployParameters(Deployer("Scheduled Deployment"), Build("testProject", "1"), Stage("TEST")))
+ }
+
+ it should "produce an error if the last deploy didn't complete" in {
+ val record = new DeployRecord(
+ new DateTime(),
+ uuid,
+ DeployParameters(Deployer("Bob"), Build("testProject", "1"), Stage("TEST")),
+ recordState = Some(RunState.Failed)
+ )
+ DeployJob.createDeployParameters(record, true) shouldBe
+ Left(Error("Skipping scheduled deploy as deploy record 7fa2ee0a-8d90-4f7e-a38b-185f36fbc5aa has status Failed"))
+ }
+
+ it should "produce an error if scheduled deploys are disabled" in {
+ val record = new DeployRecord(
+ new DateTime(),
+ uuid,
+ DeployParameters(Deployer("Bob"), Build("testProject", "1"), Stage("TEST")),
+ recordState = Some(RunState.Completed)
+ )
+ DeployJob.createDeployParameters(record, false) shouldBe
+ Left(Error("Scheduled deployments disabled. Would have deployed DeployParameters(Deployer(Scheduled Deployment),Build(testProject,1),Stage(TEST),RecipeName(default),List(),List(),All)"))
+ }
+}