From f8ac0cc92fbfdc94407ab77b301c441bdb45825e Mon Sep 17 00:00:00 2001 From: Krzysztof Pado Date: Wed, 8 Dec 2021 18:48:19 -0800 Subject: [PATCH] Support timeout for jobs and steps --- README.md | 2 ++ .../scala/sbtghactions/GenerativeKeys.scala | 4 +++ .../scala/sbtghactions/GenerativePlugin.scala | 19 +++++++++--- src/main/scala/sbtghactions/WorkflowJob.scala | 5 ++- .../scala/sbtghactions/WorkflowStep.scala | 9 ++++-- .../check-and-regenerate/build.sbt | 5 +++ .../check-and-regenerate/expected-ci.yml | 4 +++ .../sbtghactions/GenerativePluginSpec.scala | 31 +++++++++++++++++++ 8 files changed, 71 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7c10ea2..92480bd 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Any and all settings which affect the behavior of the generative plugin should b - `githubWorkflowScalaVersions` : `Seq[String]` – A list of Scala versions which will be used to `build` your project. Defaults to `crossScalaVersions` in `build`, and simply `scalaVersion` in `publish`. - `githubWorkflowOSes` : `Seq[String]` – A list of operating systems, which will be ultimately passed to [the `runs-on:` directive](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on), on which to `build` your project. Defaults to `ubuntu-latest`. Note that, regardless of the value of this setting, only `ubuntu-latest` will be used for the `publish` job. This setting only affects `build`. - `githubWorkflowBuildRunsOnExtraLabels` : `Seq[String]` - A list of additional runs-on labels, which will be combined with the matrix.os from `githubWorkflowOSes` above allowing for singling out more specific runners. +- `githubWorkflowBuildTimeout` : `Option[FiniteDuration]` - [The maximum duration](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) to let the build job run before GitHub automatically cancels it. Defaults to `None`. #### `publish` Job @@ -128,3 +129,4 @@ Any and all settings which affect the behavior of the generative plugin should b - `githubWorkflowPublish` : `Seq[WorkflowStep]` – The steps which will be invoked to publish your project. This defaults to `[sbt +publish]`. - `githubWorkflowPublishTargetBranches` : `Seq[RefPredicate]` – A list of branch predicates which will be applied to determine whether the `publish` job will run. Defaults to just `== main`. The supports all of the predicate types currently [allowed by GitHub Actions](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#functions). This exists because, while you usually want to run the `build` job on *every* branch, `publish` is obviously much more limited in applicability. If this list is empty, then the `publish` job will be omitted entirely from the workflow. - `githubWorkflowPublishCond` : `Option[String]` – This is an optional added conditional check on the publish branch, which must be defined using [GitHub Actions expression syntax](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#about-contexts-and-expressions), which will be conjoined to determine the `if:` predicate on the `publish` job. Defaults to `None`. +- `githubWorkflowPublishTimeout` : `Option[FiniteDuration]` - [The maximum duration](https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes) to let the publish job run before GitHub automatically cancels it. Defaults to `None`. \ No newline at end of file diff --git a/src/main/scala/sbtghactions/GenerativeKeys.scala b/src/main/scala/sbtghactions/GenerativeKeys.scala index 93b338c..745cf2d 100644 --- a/src/main/scala/sbtghactions/GenerativeKeys.scala +++ b/src/main/scala/sbtghactions/GenerativeKeys.scala @@ -18,6 +18,8 @@ package sbtghactions import sbt._ +import scala.concurrent.duration.FiniteDuration + trait GenerativeKeys { lazy val githubWorkflowGenerate = taskKey[Unit]("Generates (and overwrites if extant) a ci.yml and clean.yml actions description according to configuration") @@ -40,6 +42,7 @@ trait GenerativeKeys { lazy val githubWorkflowBuildPreamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after base setup but before compiling and testing (default: [])") lazy val githubWorkflowBuildPostamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after comping and testing but before the end of the build job (default: [])") + lazy val githubWorkflowBuildTimeout = settingKey[Option[FiniteDuration]]("The maximum duration to let the build job run before GitHub automatically cancels it. (default: None)") lazy val githubWorkflowBuild = settingKey[Seq[WorkflowStep]]("A sequence of workflow steps which compile and test the project (default: [Sbt(List(\"test\"))])") lazy val githubWorkflowPublishPreamble = settingKey[Seq[WorkflowStep]]("A list of steps to insert after base setup but before publishing (default: [])") @@ -47,6 +50,7 @@ trait GenerativeKeys { lazy val githubWorkflowPublish = settingKey[Seq[WorkflowStep]]("A sequence workflow steps which publishes the project (default: [Sbt(List(\"+publish\"))])") lazy val githubWorkflowPublishTargetBranches = settingKey[Seq[RefPredicate]]("A set of branch predicates which will be applied to determine whether the current branch gets a publication stage; if empty, publish will be skipped entirely (default: [== main])") lazy val githubWorkflowPublishCond = settingKey[Option[String]]("A set of conditionals to apply to the publish job to further restrict its run (default: [])") + lazy val githubWorkflowPublishTimeout = settingKey[Option[FiniteDuration]]("The maximum duration to let the publish job run before GitHub automatically cancels it. (default: None)") lazy val githubWorkflowJavaVersions = settingKey[Seq[JavaSpec]]("A list of Java versions to be used for the build job. The publish job will use the *first* of these versions. (default: [temurin@11])") lazy val githubWorkflowScalaVersions = settingKey[Seq[String]]("A list of Scala versions on which to build the project (default: crossScalaVersions.value)") diff --git a/src/main/scala/sbtghactions/GenerativePlugin.scala b/src/main/scala/sbtghactions/GenerativePlugin.scala index f635db0..bcda9f6 100644 --- a/src/main/scala/sbtghactions/GenerativePlugin.scala +++ b/src/main/scala/sbtghactions/GenerativePlugin.scala @@ -20,6 +20,7 @@ import sbt.Keys._ import sbt._ import java.nio.file.FileSystems +import scala.concurrent.duration.FiniteDuration import scala.io.Source object GenerativePlugin extends AutoPlugin { @@ -186,6 +187,10 @@ s"""$prefix: ${indent(rendered.mkString("\n"), 1)}""" } + def renderTimeout(timeout: Option[FiniteDuration], prefix: String = ""): String = { + timeout.map(_.toMinutes.toString).map(s"${prefix}timeout-minutes: " + _ + "\n").getOrElse("") + } + def compileStep(step: WorkflowStep, sbt: String, declareShell: Boolean = false): String = { import WorkflowStep._ @@ -193,6 +198,7 @@ ${indent(rendered.mkString("\n"), 1)}""" val renderedId = step.id.map(wrap).map("id: " + _ + "\n").getOrElse("") val renderedCond = step.cond.map(wrap).map("if: " + _ + "\n").getOrElse("") val renderedShell = if (declareShell) "shell: bash\n" else "" + val renderedTimeout = renderTimeout(step.timeout) val renderedEnvPre = compileEnv(step.env) val renderedEnv = if (renderedEnvPre.isEmpty) @@ -200,7 +206,7 @@ ${indent(rendered.mkString("\n"), 1)}""" else renderedEnvPre + "\n" - val preamblePre = renderedName + renderedId + renderedCond + renderedEnv + val preamblePre = renderedName + renderedId + renderedCond + renderedEnv + renderedTimeout val preamble = if (preamblePre.isEmpty) "" @@ -280,6 +286,7 @@ ${indent(rendered.mkString("\n"), 1)}""" s"\nneeds: [${job.needs.mkString(", ")}]" val renderedEnvironment = job.environment.map(compileEnvironment).map("\n" + _).getOrElse("") + val renderedTimeout = renderTimeout(job.timeout, prefix = "\n") val renderedCond = job.cond.map(wrap).map("\nif: " + _).getOrElse("") @@ -407,7 +414,7 @@ strategy:${renderedFailFast} os:${compileList(job.oses, 3)} scala:${compileList(job.scalas, 3)} java:${compileList(job.javas.map(_.render), 3)}${renderedMatrices} -runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedEnv} +runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedTimeout}${renderedEnv} steps: ${indent(job.steps.map(compileStep(_, sbt, declareShell = declareShell)).mkString("\n\n"), 1)}""" @@ -487,6 +494,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowBuildPreamble := Seq(), githubWorkflowBuildPostamble := Seq(), + githubWorkflowBuildTimeout := None, githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("test"), name = Some("Build project"))), githubWorkflowPublishPreamble := Seq(), @@ -494,6 +502,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowPublish := Seq(WorkflowStep.Sbt(List("+publish"), name = Some("Publish project"))), githubWorkflowPublishTargetBranches := Seq(RefPredicate.Equals(Ref.Branch("main"))), githubWorkflowPublishCond := None, + githubWorkflowPublishTimeout := None, githubWorkflowJavaVersions := Seq(JavaSpec.temurin("11")), githubWorkflowScalaVersions := crossScalaVersions.value, @@ -661,7 +670,8 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} cond = Some(s"github.event_name != 'pull_request' && $publicationCond"), scalas = List(scalaVersion.value), javas = List(githubWorkflowJavaVersions.value.head), - needs = List("build"))).filter(_ => !githubWorkflowPublishTargetBranches.value.isEmpty) + needs = List("build"), + timeout = githubWorkflowPublishTimeout.value)).filter(_ => !githubWorkflowPublishTargetBranches.value.isEmpty) Seq( WorkflowJob( @@ -682,7 +692,8 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} matrixAdds = githubWorkflowBuildMatrixAdditions.value, matrixIncs = githubWorkflowBuildMatrixInclusions.value.toList, matrixExcs = githubWorkflowBuildMatrixExclusions.value.toList, - runsOnExtraLabels = githubWorkflowBuildRunsOnExtraLabels.value.toList )) ++ publishJobOpt ++ githubWorkflowAddedJobs.value + runsOnExtraLabels = githubWorkflowBuildRunsOnExtraLabels.value.toList, + timeout = githubWorkflowBuildTimeout.value)) ++ publishJobOpt ++ githubWorkflowAddedJobs.value }) private val generateCiContents = Def task { diff --git a/src/main/scala/sbtghactions/WorkflowJob.scala b/src/main/scala/sbtghactions/WorkflowJob.scala index c1b8c20..4a412c5 100644 --- a/src/main/scala/sbtghactions/WorkflowJob.scala +++ b/src/main/scala/sbtghactions/WorkflowJob.scala @@ -16,6 +16,8 @@ package sbtghactions +import scala.concurrent.duration.FiniteDuration + final case class WorkflowJob( id: String, name: String, @@ -32,4 +34,5 @@ final case class WorkflowJob( matrixExcs: List[MatrixExclude] = List(), runsOnExtraLabels: List[String] = List(), container: Option[JobContainer] = None, - environment: Option[JobEnvironment] = None) + environment: Option[JobEnvironment] = None, + timeout: Option[FiniteDuration] = None) diff --git a/src/main/scala/sbtghactions/WorkflowStep.scala b/src/main/scala/sbtghactions/WorkflowStep.scala index 8dff410..b849c68 100644 --- a/src/main/scala/sbtghactions/WorkflowStep.scala +++ b/src/main/scala/sbtghactions/WorkflowStep.scala @@ -16,11 +16,14 @@ package sbtghactions +import scala.concurrent.duration.FiniteDuration + sealed trait WorkflowStep extends Product with Serializable { def id: Option[String] def name: Option[String] def cond: Option[String] def env: Map[String, String] + def timeout: Option[FiniteDuration] } object WorkflowStep { @@ -65,7 +68,7 @@ object WorkflowStep { List("echo \"$(" + cmd + ")\" >> $GITHUB_PATH"), name = Some(s"Prepend $$PATH using $cmd")) - final case class Run(commands: List[String], id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), params: Map[String, String] = Map()) extends WorkflowStep - final case class Sbt(commands: List[String], id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), params: Map[String, String] = Map()) extends WorkflowStep - final case class Use(ref: UseRef, params: Map[String, String] = Map(), id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map()) extends WorkflowStep + final case class Run(commands: List[String], id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), params: Map[String, String] = Map(), timeout: Option[FiniteDuration] = None) extends WorkflowStep + final case class Sbt(commands: List[String], id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), params: Map[String, String] = Map(), timeout: Option[FiniteDuration] = None) extends WorkflowStep + final case class Use(ref: UseRef, params: Map[String, String] = Map(), id: Option[String] = None, name: Option[String] = None, cond: Option[String] = None, env: Map[String, String] = Map(), timeout: Option[FiniteDuration] = None) extends WorkflowStep } diff --git a/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt b/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt index 207a426..86f695f 100644 --- a/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt +++ b/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt @@ -1,3 +1,5 @@ +import scala.concurrent.duration._ + organization := "com.codecommit" version := "0.0.1" @@ -19,4 +21,7 @@ ThisBuild / githubWorkflowBuildMatrixExclusions += MatrixExclude(Map("scala" -> "2.12.15", "test" -> "is")) ThisBuild / githubWorkflowBuild += WorkflowStep.Run(List("echo yo")) +ThisBuild / githubWorkflowBuildTimeout := Some(2.hours) + ThisBuild / githubWorkflowPublish += WorkflowStep.Run(List("echo sup")) +ThisBuild / githubWorkflowPublishTimeout := Some(1.hour) diff --git a/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml b/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml index ee1ed2d..2a31a86 100644 --- a/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml +++ b/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml @@ -33,6 +33,8 @@ jobs: - scala: 2.12.15 test: is runs-on: ${{ matrix.os }} + timeout-minutes: 120 + steps: - name: Checkout current branch (full) uses: actions/checkout@v2 @@ -92,6 +94,8 @@ jobs: scala: [2.13.6] java: [temurin@11] runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: - name: Checkout current branch (full) uses: actions/checkout@v2 diff --git a/src/test/scala/sbtghactions/GenerativePluginSpec.scala b/src/test/scala/sbtghactions/GenerativePluginSpec.scala index e544d96..1af271a 100644 --- a/src/test/scala/sbtghactions/GenerativePluginSpec.scala +++ b/src/test/scala/sbtghactions/GenerativePluginSpec.scala @@ -19,6 +19,7 @@ package sbtghactions import org.specs2.mutable.Specification import java.net.URL +import scala.concurrent.duration.DurationInt class GenerativePluginSpec extends Specification { import GenerativePlugin._ @@ -450,6 +451,12 @@ class GenerativePluginSpec extends Specification { | abc: def | cafe: '@42'""".stripMargin } + + "compile a run step with a timeout" in { + compileStep( + Run(List("users"), timeout = Some(1.hour)), + "") mustEqual "- timeout-minutes: 60\n run: users" + } } "job compilation" should { @@ -662,6 +669,30 @@ class GenerativePluginSpec extends Specification { - run: echo hello""" } + "compile a job with a timeout" in { + val results = compileJob( + WorkflowJob( + "publish", + "Publish Release", + List( + WorkflowStep.Sbt(List("ci-release"))), + timeout = Some(1.hour)), + "csbt") + + results mustEqual s"""publish: + name: Publish Release + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.6] + java: [temurin@11] + runs-on: $${{ matrix.os }} + timeout-minutes: 60 + + steps: + - run: csbt ++$${{ matrix.scala }} ci-release""" + } + "produce an error when compiling a job with `include` key in matrix" in { compileJob( WorkflowJob(