diff --git a/README.md b/README.md index b24b341..a85d497 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Any and all settings which affect the behavior of the generative plugin should b #### `build` Job - `githubWorkflowBuildMatrixAdditions` : `Map[String, List[String]]` – Contains a map of additional `matrix:` dimensions which will be added to the `build` job (on top of the auto-generated ones for Scala/Java/JVM version). As an example, this can be used to manually achieve additional matrix expansion for ScalaJS compilation. Matrix variables can be referenced in the conventional way within steps by using the `${{ matrix.keynamehere }}` syntax. Defaults to empty. +- `githubWorkflowBuildMatrixInclusions` : `Seq[MatrixInclude]` – A list of [matrix *inclusions*](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-including-additional-values-into-combinations). This is useful for when you have a specific matrix job which needs to do extra work, or wish to add an individual matrix job to the configuration set. The matching keys and values are verified against the known matrix configuration. Defaults to empty. +- `githubWorkflowBuildMatrixExclusions` : `Seq[MatrixExclude]` – A list of [matrix *exclusions*](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#example-excluding-configurations-from-a-matrix). This is useful for when there is a matrix expansion (or set of expansions) which you wish to filter out of the set. Note that exclusions are applied *before* inclusions, allowing you to subtract jobs before re-adding them. Also – and the documentation isn't clear on this point – it is possible that the matching must cover the full set of matrix keys and cannot contain partial values. Defaults to empty. - `githubWorkflowBuildPreamble` : `Seq[WorkflowStep]` – Contains a list of steps which will be inserted into the `build` job in the **ci.yml** workflow *after* setup but *before* the `sbt test` invocation. Defaults to empty. - `githubWorkflowBuildPostamble` : `Seq[WorkflowStep]` – Similar to the `Preamble` variant, this contains a list of steps which will be added to the `build` job *after* the `sbt test` invocation but before cleanup. Defaults to empty. - `githubWorkflowBuild` : `Seq[WorkflowStep]` – The steps which invoke sbt (or whatever else you want) to build and test your project. This defaults to just `[sbt test]`, but can be overridden to anything. For example, sbt plugin projects probably want to redefine this to be `Seq(WorkflowStep.Sbt(List("test", "scripted")))`, which would run the `test` and `scripted` sbt tasks, in order. Note that all uses of `WorkflowStep.Sbt` are compiled using the configured `githubWorkflowSbtCommand` invocation, and properly configured with respect to the build matrix-selected Scala version. diff --git a/build.sbt b/build.sbt index de71b5e..8fa55d8 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ name := "sbt-github-actions" -ThisBuild / baseVersion := "0.7" +ThisBuild / baseVersion := "0.8" ThisBuild / organization := "com.codecommit" ThisBuild / publishGithubUser := "djspiewak" diff --git a/src/main/scala/sbtghactions/GenerativeKeys.scala b/src/main/scala/sbtghactions/GenerativeKeys.scala index 89e51fe..9e37d40 100644 --- a/src/main/scala/sbtghactions/GenerativeKeys.scala +++ b/src/main/scala/sbtghactions/GenerativeKeys.scala @@ -31,6 +31,9 @@ trait GenerativeKeys { lazy val githubWorkflowSbtCommand = settingKey[String]("The command which invokes sbt (default: sbt") lazy val githubWorkflowBuildMatrixAdditions = settingKey[Map[String, List[String]]]("A map of additional matrix dimensions for the build job. Each list should be non-empty. (default: {})") + lazy val githubWorkflowBuildMatrixInclusions = settingKey[Seq[MatrixInclude]]("A list of matrix inclusions (default: [])") + lazy val githubWorkflowBuildMatrixExclusions = settingKey[Seq[MatrixExclude]]("A list of matrix exclusions (default: [])") + 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 githubWorkflowBuild = settingKey[Seq[WorkflowStep]]("A sequence of workflow steps which compile and test the project (default: [Sbt(List(\"test\"))])") diff --git a/src/main/scala/sbtghactions/GenerativePlugin.scala b/src/main/scala/sbtghactions/GenerativePlugin.scala index c9c5cb3..38ddd86 100644 --- a/src/main/scala/sbtghactions/GenerativePlugin.scala +++ b/src/main/scala/sbtghactions/GenerativePlugin.scala @@ -43,6 +43,12 @@ object GenerativePlugin extends AutoPlugin { type PREventType = sbtghactions.PREventType val PREventType = sbtghactions.PREventType + + type MatrixInclude = sbtghactions.MatrixInclude + val MatrixInclude = sbtghactions.MatrixInclude + + type MatrixExclude = sbtghactions.MatrixExclude + val MatrixExclude = sbtghactions.MatrixExclude } import autoImport._ @@ -88,6 +94,15 @@ object GenerativePlugin extends AutoPlugin { "\n" + indent(rendered.map("- " + _).mkString("\n"), level) } + def compileListOfSimpleDicts(items: List[Map[String, String]]): String = + items map { dict => + val rendered = dict map { + case (key, value) => s"$key: $value" + } mkString "\n" + + "-" + indent(rendered, 1).substring(1) + } mkString "\n" + def compilePREventType(tpe: PREventType): String = { import PREventType._ @@ -211,14 +226,66 @@ ${indent(rendered.mkString("\n"), 1)}""" else "\n" + renderedEnvPre + List("include", "exclude") foreach { key => + if (job.matrixAdds.contains(key)) { + sys.error(s"key `$key` is reserved and cannot be used in an Actions matrix definition") + } + } + val renderedMatricesPre = job.matrixAdds map { case (key, values) => s"$key: ${values.map(wrap).mkString("[", ", ", "]")}" } mkString "\n" - val renderedMatrices = if (renderedMatricesPre.isEmpty) + // TODO refactor all of this stuff to use whitelist instead + val whitelist = Map("os" -> job.oses, "scala" -> job.scalas, "java" -> job.javas) ++ job.matrixAdds + + def checkMatching(matching: Map[String, String]): Unit = { + matching foreach { + case (key, value) => + if (!whitelist.contains(key)) { + sys.error(s"inclusion key `$key` was not found in matrix") + } + + if (!whitelist(key).contains(value)) { + sys.error(s"inclusion key `$key` was present in matrix, but value `$value` was not in ${whitelist(key)}") + } + } + } + + val renderedIncludesPre = if (job.matrixIncs.isEmpty) { + renderedMatricesPre + } else { + job.matrixIncs.foreach(inc => checkMatching(inc.matching)) + + val rendered = compileListOfSimpleDicts(job.matrixIncs.map(i => i.matching ++ i.additions)) + + val renderedMatrices = if (renderedMatricesPre.isEmpty) + "" + else + renderedMatricesPre + "\n" + + s"${renderedMatrices}include:\n${indent(rendered, 1)}" + } + + val renderedExcludesPre = if (job.matrixExcs.isEmpty) { + renderedIncludesPre + } else { + job.matrixExcs.foreach(exc => checkMatching(exc.matching)) + + val rendered = compileListOfSimpleDicts(job.matrixExcs.map(_.matching)) + + val renderedIncludes = if (renderedIncludesPre.isEmpty) + "" + else + renderedIncludesPre + "\n" + + s"${renderedIncludes}exclude:\n${indent(rendered, 1)}" + } + + val renderedMatrices = if (renderedExcludesPre.isEmpty) "" else - "\n" + indent(renderedMatricesPre, 2) + "\n" + indent(renderedExcludesPre, 2) val declareShell = job.oses.exists(_.contains("windows")) @@ -279,6 +346,9 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)}""" githubWorkflowSbtCommand := "sbt", githubWorkflowBuildMatrixAdditions := Map(), + githubWorkflowBuildMatrixInclusions := Seq(), + githubWorkflowBuildMatrixExclusions := Seq(), + githubWorkflowBuildPreamble := Seq(), githubWorkflowBuildPostamble := Seq(), githubWorkflowBuild := Seq(WorkflowStep.Sbt(List("test"), name = Some("Build project"))), @@ -558,7 +628,9 @@ git config --global alias.rm-symlink '!git rm-symlinks' # for back-compat.""" oses = githubWorkflowOSes.value.toList, scalas = crossScalaVersions.value.toList, javas = githubWorkflowJavaVersions.value.toList, - matrixAdds = githubWorkflowBuildMatrixAdditions.value)) ++ publishJobOpt ++ githubWorkflowAddedJobs.value + matrixAdds = githubWorkflowBuildMatrixAdditions.value, + matrixIncs = githubWorkflowBuildMatrixInclusions.value.toList, + matrixExcs = githubWorkflowBuildMatrixExclusions.value.toList)) ++ 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 d7c8f11..c1e4b57 100644 --- a/src/main/scala/sbtghactions/WorkflowJob.scala +++ b/src/main/scala/sbtghactions/WorkflowJob.scala @@ -26,4 +26,6 @@ final case class WorkflowJob( scalas: List[String] = List("2.13.1"), javas: List[String] = List("adopt@1.8"), needs: List[String] = Nil, - matrixAdds: Map[String, List[String]] = Map()) + matrixAdds: Map[String, List[String]] = Map(), + matrixIncs: List[MatrixInclude] = List(), + matrixExcs: List[MatrixExclude] = List()) diff --git a/src/main/scala/sbtghactions/matrix.scala b/src/main/scala/sbtghactions/matrix.scala new file mode 100644 index 0000000..a888e23 --- /dev/null +++ b/src/main/scala/sbtghactions/matrix.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Daniel Spiewak + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sbtghactions + +final case class MatrixInclude( + matching: Map[String, String], + additions: Map[String, String]) + +final case class MatrixExclude(matching: Map[String, String]) diff --git a/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt b/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt index 39785cb..19346a7 100644 --- a/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt +++ b/src/sbt-test/sbtghactions/check-and-regenerate/build.sbt @@ -9,5 +9,12 @@ ThisBuild / githubWorkflowPublishTargetBranches += RefPredicate.Equals(Ref.Tag(" ThisBuild / githubWorkflowBuildMatrixAdditions += "test" -> List("this", "is") +ThisBuild / githubWorkflowBuildMatrixInclusions += MatrixInclude( + Map("test" -> "this"), + Map("extra" -> "sparta")) + +ThisBuild / githubWorkflowBuildMatrixExclusions += + MatrixExclude(Map("scala" -> "2.12.10", "test" -> "is")) + ThisBuild / githubWorkflowBuild += WorkflowStep.Run(List("echo yo")) ThisBuild / githubWorkflowPublish += WorkflowStep.Run(List("echo sup")) 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 198de21..14fb4ab 100644 --- a/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml +++ b/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml @@ -25,6 +25,12 @@ jobs: scala: [2.13.1, 2.12.10] java: [adopt@1.8, graalvm@20.0.0] test: [this, is] + include: + - test: this + extra: sparta + exclude: + - scala: 2.12.10 + test: is runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (fast) diff --git a/src/test/scala/sbtghactions/GenerativePluginSpec.scala b/src/test/scala/sbtghactions/GenerativePluginSpec.scala index b935918..a0a2191 100644 --- a/src/test/scala/sbtghactions/GenerativePluginSpec.scala +++ b/src/test/scala/sbtghactions/GenerativePluginSpec.scala @@ -371,6 +371,134 @@ class GenerativePluginSpec extends Specification { uses: actions/checkout@v2""" } + "produce an error when compiling a job with `include` key in matrix" in { + compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List(), + matrixAdds = Map("include" -> List("1", "2"))), + "") must throwA[RuntimeException] + } + + "produce an error when compiling a job with `exclude` key in matrix" in { + compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List(), + matrixAdds = Map("exclude" -> List("1", "2"))), + "") must throwA[RuntimeException] + } + + "compile a job with a simple matching inclusion" in { + val results = compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List( + WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), + matrixIncs = List( + MatrixInclude( + Map("scala" -> "2.13.1"), + Map("foo" -> "bar")))), + "") + + results mustEqual s"""bippy: + name: Bippity Bop Around the Clock + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.1] + java: [adopt@1.8] + include: + - scala: 2.13.1 + foo: bar + runs-on: $${{ matrix.os }} + steps: + - run: echo $${{ matrix.scala }}""" + } + + "produce an error with a non-matching inclusion key" in { + compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List( + WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), + matrixIncs = List( + MatrixInclude( + Map("scalanot" -> "2.13.1"), + Map("foo" -> "bar")))), + "") must throwA[RuntimeException] + } + + "produce an error with a non-matching inclusion value" in { + compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List( + WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), + matrixIncs = List( + MatrixInclude( + Map("scala" -> "0.12.1"), + Map("foo" -> "bar")))), + "") must throwA[RuntimeException] + } + + "compile a job with a simple matching exclusion" in { + val results = compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List( + WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), + matrixExcs = List( + MatrixExclude( + Map("scala" -> "2.13.1")))), + "") + + results mustEqual s"""bippy: + name: Bippity Bop Around the Clock + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.1] + java: [adopt@1.8] + exclude: + - scala: 2.13.1 + runs-on: $${{ matrix.os }} + steps: + - run: echo $${{ matrix.scala }}""" + } + + "produce an error with a non-matching exclusion key" in { + compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List( + WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), + matrixExcs = List( + MatrixExclude( + Map("scalanot" -> "2.13.1")))), + "") must throwA[RuntimeException] + } + + "produce an error with a non-matching exclusion value" in { + compileJob( + WorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + List( + WorkflowStep.Run(List("echo ${{ matrix.scala }}"))), + matrixExcs = List( + MatrixExclude( + Map("scala" -> "0.12.1")))), + "") must throwA[RuntimeException] + } + "compile a job with illegal characters in the JVM" in { val results = compileJob( WorkflowJob(