diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfa6e8f..fdd015e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,8 @@ on: push: branches: ['**'] tags: [v*] - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: build: name: Build and Test diff --git a/README.md b/README.md index 50d58df..2371745 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ ThisBuild / githubWorkflowPublish := Seq( ### Generative -- `githubWorkflowGenerate` – Generates (and overwrites if extant) **ci.yml** and **clean.yml** workflows according to configuration within sbt. The **clean.yml** workflow is something that GitHub Actions should just do by default: it removes old build artifacts to prevent them from running up your storage usage (it has no effect on currently running builds). This workflow is unconfigurable and is simply drawn from the static contents of the **clean.yml** resource file within this repository. +- `githubWorkflowGenerate` – Generates (and overwrites if extant) **ci.yml**, **clean.yml** and all custom (see `githubWorkflowCustomWorkflows`) workflows according to configuration within sbt. The **clean.yml** workflow is something that GitHub Actions should just do by default: it removes old build artifacts to prevent them from running up your storage usage (it has no effect on currently running builds). This workflow is unconfigurable and is simply drawn from the static contents of the **clean.yml** resource file within this repository. - `githubWorkflowCheck` – Checks to see if the **ci.yml** and **clean.yml** files are equivalent to what would be generated and errors if otherwise. This task is run from within the generated **ci.yml** to ensure that the build and the workflow are kept in sync. As a general rule, any time you change the workflow configuration within sbt, you should regenerate the **ci.yml** and commit the results, but inevitably people forget. This check fails the build if that happens. Note that if you *need* to manually fiddle with the **ci.yml** contents, for whatever reason, you will need to remove the call to this check from within the workflow, otherwise your build will simply fail. ## Settings @@ -99,6 +99,7 @@ Any and all settings which affect the behavior of the generative plugin should b #### General +- `githubWorkflowGenerationTargets` : `Set[GenerationTarget]` — A set of targets to be generated. Possible values are `CI`, `Clean` and `Custom`. - `githubWorkflowGeneratedCI` : `Seq[WorkflowJob]` — Contains a description of the **ci.yml** jobs that will drive the generation if used. This setting can be overridden to customize the jobs (e.g. by adding additional jobs to the workflow). - `githubWorkflowGeneratedUploadSteps` : `Seq[WorkflowStep]` – Contains a list of steps which are used to upload generated intermediate artifacts from the `build` job. This is mostly for reference and introspection purposes; one would not be expected to *change* this setting. - `githubWorkflowGeneratedDownloadSteps` : `Seq[WorkflowStep]` – Contains a list of steps which are used to download generated intermediate artifacts from the `build` job. This is mostly for reference and introspection purposes; one would not be expected to *change* this setting. This setting is particularly useful in conjunction with `githubWorkflowAddedJobs`: if you're adding a job which needs access to intermediate artifacts, you should make sure these steps are part of the process. @@ -115,6 +116,7 @@ Any and all settings which affect the behavior of the generative plugin should b - `githubWorkflowJobSetup` : `Seq[WorkflowStep]` – The automatically-generated checkout, setup, and cache steps which are common to all jobs which touch the build (default: autogenerated) - `githubWorkflowEnv` : `Map[String, String]` – An environment which is global to the entire **ci.yml** workflow. Defaults to `Map("GITHUB_TOKEN" -> "${{ secrets.GITHUB_TOKEN }}")` since it's so commonly needed. - `githubWorkflowAddedJobs` : `Seq[WorkflowJob]` – A convenience mechanism for adding extra custom jobs to the **ci.yml** workflow (though you can also do this by modifying `githubWorkflowGeneratedCI`). Defaults to empty. +- `githubWorkflowCustomWorkflows`: `Map[String, Workflow]` - This is the place to define your custom workflows. The key represents the filename (**.yml** is added if not provided) the value the workflow definition. The settings of the generative plugin apply, as long as they are not part of the workflow trigger events. For example, the `githubWorkflowTargetBranches` setting has no influence on custom workflows, but `githubWorkflowScalaVersions` does. #### `build` Job diff --git a/src/main/scala/sbtghactions/CheckRunEventType.scala b/src/main/scala/sbtghactions/CheckRunEventType.scala new file mode 100644 index 0000000..1ae5dcc --- /dev/null +++ b/src/main/scala/sbtghactions/CheckRunEventType.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#check_run + */ +sealed trait CheckRunEventType extends EventType + +object CheckRunEventType { + case object Created extends CheckRunEventType + case object Rerequested extends CheckRunEventType + case object Completed extends CheckRunEventType +} diff --git a/src/main/scala/sbtghactions/CheckSuiteEventType.scala b/src/main/scala/sbtghactions/CheckSuiteEventType.scala new file mode 100644 index 0000000..259d0c3 --- /dev/null +++ b/src/main/scala/sbtghactions/CheckSuiteEventType.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#check_run + */ +sealed trait CheckSuiteEventType extends EventType + +object CheckSuiteEventType { + case object Completed extends CheckSuiteEventType + case object Requested extends CheckSuiteEventType + case object Rerequested extends CheckSuiteEventType +} diff --git a/src/main/scala/sbtghactions/EventType.scala b/src/main/scala/sbtghactions/EventType.scala new file mode 100644 index 0000000..b7cb927 --- /dev/null +++ b/src/main/scala/sbtghactions/EventType.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2021 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 + +import sbtghactions.RenderFunctions.SnakeCase + +trait EventType extends Product with Serializable { + def render: String = SnakeCase(productPrefix) +} diff --git a/src/main/scala/sbtghactions/GenerationTarget.scala b/src/main/scala/sbtghactions/GenerationTarget.scala new file mode 100644 index 0000000..5face1c --- /dev/null +++ b/src/main/scala/sbtghactions/GenerationTarget.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2021 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 + + +sealed trait GenerationTarget extends Product with Serializable + +object GenerationTarget { + val all: Set[GenerationTarget] = Set(GenerationTarget.CI, GenerationTarget.Clean, GenerationTarget.Custom) + + case object CI extends GenerationTarget + case object Clean extends GenerationTarget + case object Custom extends GenerationTarget +} diff --git a/src/main/scala/sbtghactions/GenerativeKeys.scala b/src/main/scala/sbtghactions/GenerativeKeys.scala index ab2c717..c4b0af6 100644 --- a/src/main/scala/sbtghactions/GenerativeKeys.scala +++ b/src/main/scala/sbtghactions/GenerativeKeys.scala @@ -23,11 +23,14 @@ trait GenerativeKeys { lazy val githubWorkflowGenerate = taskKey[Unit]("Generates (and overwrites if extant) a ci.yml and clean.yml actions description according to configuration") lazy val githubWorkflowCheck = taskKey[Unit]("Checks to see if the ci.yml and clean.yml files are equivalent to what would be generated and errors if otherwise") + lazy val githubWorkflowGenerationTargets = settingKey[Set[GenerationTarget]]("Configure targets to be generated. (CI, Clean or Custom)") lazy val githubWorkflowGeneratedCI = settingKey[Seq[WorkflowJob]]("The sequence of jobs which will make up the generated ci workflow (ci.yml)") lazy val githubWorkflowGeneratedUploadSteps = settingKey[Seq[WorkflowStep]]("The sequence of steps used to upload intermediate build artifacts for an adjacent job") lazy val githubWorkflowGeneratedDownloadSteps = settingKey[Seq[WorkflowStep]]("The sequence of steps used to download intermediate build artifacts published by an adjacent job") lazy val githubWorkflowGeneratedCacheSteps = settingKey[Seq[WorkflowStep]]("The sequence of steps used to configure caching for ivy, sbt, and coursier") + lazy val githubWorkflowCustomWorkflows = settingKey[Map[String, Workflow]]("Custom workflows, defined by a map of file name to Workflow.") + lazy val githubWorkflowSbtCommand = settingKey[String]("The command which invokes sbt (default: sbt)") lazy val githubWorkflowUseSbtThinClient = settingKey[Boolean]("Whether to use sbt's native thin client, default is false since this can cause issues (see https://github.com/sbt/sbt/issues/6468)") lazy val githubWorkflowIncludeClean = settingKey[Boolean]("Whether to include the clean.yml file (default: true)") diff --git a/src/main/scala/sbtghactions/GenerativePlugin.scala b/src/main/scala/sbtghactions/GenerativePlugin.scala index 6bf6751..e01698f 100644 --- a/src/main/scala/sbtghactions/GenerativePlugin.scala +++ b/src/main/scala/sbtghactions/GenerativePlugin.scala @@ -16,8 +16,9 @@ package sbtghactions -import sbt.Keys._ -import sbt._ +import sbt.* +import sbt.Keys.* +import sbtghactions.RenderFunctions.* import java.nio.file.FileSystems import scala.io.Source @@ -62,41 +63,13 @@ object GenerativePlugin extends AutoPlugin { val JavaSpec = sbtghactions.JavaSpec } - import autoImport._ + import autoImport.* private def indent(output: String, level: Int): String = { val space = (0 until level * 2).map(_ => ' ').mkString (space + output.replace("\n", s"\n$space")).replaceAll("""\n[ ]+\n""", "\n\n") } - private def isSafeString(str: String): Boolean = - !(str.indexOf(':') >= 0 || // pretend colon is illegal everywhere for simplicity - str.indexOf('#') >= 0 || // same for comment - str.indexOf('!') == 0 || - str.indexOf('*') == 0 || - str.indexOf('-') == 0 || - str.indexOf('?') == 0 || - str.indexOf('{') == 0 || - str.indexOf('}') == 0 || - str.indexOf('[') == 0 || - str.indexOf(']') == 0 || - str.indexOf(',') == 0 || - str.indexOf('|') == 0 || - str.indexOf('>') == 0 || - str.indexOf('@') == 0 || - str.indexOf('`') == 0 || - str.indexOf('"') == 0 || - str.indexOf('\'') == 0 || - str.indexOf('&') == 0) - - private def wrap(str: String): String = - if (str.indexOf('\n') >= 0) - "|\n" + indent(str, 1) - else if (isSafeString(str)) - str - else - s"'${str.replace("'", "''")}'" - def compileList(items: List[String], level: Int): String = { val rendered = items.map(wrap) if (rendered.map(_.length).sum < 40) // just arbitrarily... @@ -115,7 +88,7 @@ object GenerativePlugin extends AutoPlugin { } mkString "\n" def compilePREventType(tpe: PREventType): String = { - import PREventType._ + import PREventType.* tpe match { case Assigned => "assigned" @@ -171,77 +144,20 @@ object GenerativePlugin extends AutoPlugin { s"environment: ${wrap(environment.name)}" } - def compileEnv(env: Map[String, String], prefix: String = "env"): String = - if (env.isEmpty) { - "" - } else { - val rendered = env map { - case (key, value) => - if (!isSafeString(key) || key.indexOf(' ') >= 0) - sys.error(s"'$key' is not a valid environment variable name") - - s"""$key: ${wrap(value)}""" - } -s"""$prefix: -${indent(rendered.mkString("\n"), 1)}""" - } - - def compilePermissionScope(permissionScope: PermissionScope): String = permissionScope match { - case PermissionScope.Actions => "actions" - case PermissionScope.Checks => "checks" - case PermissionScope.Contents => "contents" - case PermissionScope.Deployments => "deployments" - case PermissionScope.IdToken => "id-token" - case PermissionScope.Issues => "issues" - case PermissionScope.Discussions => "discussions" - case PermissionScope.Packages => "packages" - case PermissionScope.Pages => "pages" - case PermissionScope.PullRequests => "pull-requests" - case PermissionScope.RepositoryProjects => "repository-projects" - case PermissionScope.SecurityEvents => "security-events" - case PermissionScope.Statuses => "statuses" - } - - def compilePermissionsValue(permissionValue: PermissionValue): String = permissionValue match { - case PermissionValue.Read => "read" - case PermissionValue.Write => "write" - case PermissionValue.None => "none" - } - - def compilePermissions(permissions: Option[Permissions]): String = { - permissions match { - case Some(perms) => - val rendered = perms match { - case Permissions.ReadAll => " read-all" - case Permissions.WriteAll => " write-all" - case Permissions.None => " {}" - case Permissions.Specify(permMap) => - val map = permMap.map{ - case (key, value) => - s"${compilePermissionScope(key)}: ${compilePermissionsValue(value)}" - } - "\n" + indent(map.mkString("\n"), 1) - } - s"permissions:$rendered" - - case None => "" - } - } def compileStep( step: WorkflowStep, sbt: String, sbtStepPreamble: List[String] = WorkflowStep.DefaultSbtStepPreamble, - declareShell: Boolean = false - ): String = { - import WorkflowStep._ + declareShell: Boolean = false): String = { + import WorkflowStep.* val renderedName = step.name.map(wrap).map("name: " + _ + "\n").getOrElse("") 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 renderedEnvPre = compileEnv(step.env) + val renderedEnvPre = step.renderEnv val renderedEnv = if (renderedEnvPre.isEmpty) "" else @@ -278,9 +194,7 @@ ${indent(rendered.mkString("\n"), 1)}""" ) case use: Use => - import use.{ref, params} - - val decl = ref match { + val decl = use.ref match { case UseRef.Public(owner, repo, ref) => s"uses: $owner/$repo@$ref" @@ -299,27 +213,30 @@ ${indent(rendered.mkString("\n"), 1)}""" s"uses: docker://$image:$tag" } - decl + renderParams(params) + decl + use.renderParams } - indent(preamble + body, 1).updated(0, '-') + indentOnce(preamble + body).updated(0, '-') } def renderRunBody(commands: List[String], params: Map[String, String], renderedShell: String) = - renderedShell + "run: " + wrap(commands.mkString("\n")) + renderParams(params) + renderedShell + "run: " + wrap(commands.mkString("\n")) + renderMap(params, "with") - def renderParams(params: Map[String, String]): String = { - val renderedParamsPre = compileEnv(params, prefix = "with") - val renderedParams = if (renderedParamsPre.isEmpty) - "" - else - "\n" + renderedParamsPre - - renderedParams - } + def compileJob(job: WorkflowJobBase, sbt: String): String = + job match { + case ReusableWorkflowJob(id, name, uses, cond, needs) => + val renderedNeeds = if (needs.isEmpty) + "" + else + s"\nneeds: [${needs.mkString(", ")}]" + val renderedCond = cond.map(wrap).map("\nif: " + _).getOrElse("") + val body = + s"""|name: ${wrap(name)}$renderedNeeds$renderedCond + |${uses.render}""".stripMargin - def compileJob(job: WorkflowJob, sbt: String): String = { + s"$id:\n${indent(body, 1)}" + case job: WorkflowJob => val renderedNeeds = if (job.needs.isEmpty) "" else @@ -344,10 +261,7 @@ ${indent(rendered.mkString("\n"), 1)}""" "" } - val renderedEnv = if (!env.isEmpty) - "\n" + compileEnv(env) - else - "" + val renderedEnv = renderMap(env, "env") val renderedVolumes = if (!volumes.isEmpty) s"\nvolumes:${compileList(volumes.toList map { case (l, r) => s"$l:$r" }, 1)}" @@ -371,17 +285,8 @@ ${indent(rendered.mkString("\n"), 1)}""" "" } - val renderedEnvPre = compileEnv(job.env) - val renderedEnv = if (renderedEnvPre.isEmpty) - "" - else - "\n" + renderedEnvPre - - val renderedPermPre = compilePermissions(job.permissions) - val renderedPerm = if (renderedPermPre.isEmpty) - "" - else - "\n" + renderedPermPre + val renderedEnv = renderMap(job.env, "env") + val renderedPerm = job.permissions.map(_.render).mkString List("include", "exclude") foreach { key => if (job.matrixAdds.contains(key)) { @@ -446,69 +351,56 @@ ${indent(rendered.mkString("\n"), 1)}""" val declareShell = job.oses.exists(_.contains("windows")) + val matrixOs = if(job.oses.isEmpty) Nil else List("\"${{ matrix.os }}\"") + val runsOn = if (job.runsOnExtraLabels.isEmpty) s"$${{ matrix.os }}" else - job.runsOnExtraLabels.mkString(s"""[ "$${{ matrix.os }}", """, ", ", " ]" ) + (matrixOs ++ job.runsOnExtraLabels).mkString(s"""[ """, ", ", " ]" ) val renderedFailFast = job.matrixFailFast.fold("")("\n fail-fast: " + _) - val body = s"""name: ${wrap(job.name)}${renderedNeeds}${renderedCond} -strategy:${renderedFailFast} - matrix: - os:${compileList(job.oses, 3)} - scala:${compileList(job.scalas, 3)} - java:${compileList(job.javas.map(_.render), 3)}${renderedMatrices} -runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedPerm}${renderedEnv} -steps: -${indent(job.steps.map(compileStep(_, sbt, job.sbtStepPreamble, declareShell = declareShell)).mkString("\n\n"), 1)}""" - - s"${job.id}:\n${indent(body, 1)}" - } + val renderedOses = + if (job.oses.isEmpty) "" else s"\n os:${compileList(job.oses, 3)}" + val renderedScalas = + if (job.scalas.isEmpty) "" else s"\n scala:${compileList(job.scalas, 3)}" + val renderedJavas = + if (job.javas.isEmpty) "" else s"\n java:${compileList(job.javas.map(_.render), 3)}" + val renderedMatrix = + if(renderedOses.nonEmpty || renderedScalas.nonEmpty || renderedJavas.nonEmpty) { + s"\n matrix:$renderedOses$renderedScalas$renderedJavas$renderedMatrices" + } else { + "" + } - def compileWorkflow( - name: String, - branches: List[String], - tags: List[String], - paths: Paths, - prEventTypes: List[PREventType], - permissions: Option[Permissions], - env: Map[String, String], - jobs: List[WorkflowJob], - sbt: String) - : String = { - - val renderedPermissionsPre = compilePermissions(permissions) - val renderedEnvPre = compileEnv(env) - val renderedEnv = if (renderedEnvPre.isEmpty) - "" - else - renderedEnvPre + "\n\n" - val renderedPerm = if (renderedPermissionsPre.isEmpty) - "" - else - renderedPermissionsPre + "\n\n" + val renderedStrategy = + if(renderedFailFast.nonEmpty || renderedMatrix.nonEmpty) { + s"\nstrategy:$renderedFailFast$renderedMatrix" + } else { + "" + } - val renderedTypesPre = prEventTypes.map(compilePREventType).mkString("[", ", ", "]") - val renderedTypes = if (prEventTypes.sortBy(_.toString) == PREventType.Defaults) - "" - else - "\n" + indent("types: " + renderedTypesPre, 2) + val renderedSteps = + if (job.steps.nonEmpty) { + s"""steps: + |${indent( + job.steps.map( + compileStep(_, sbt, job.sbtStepPreamble, declareShell) + ).mkString("\n\n"), + 1) + }""".stripMargin + } else { + "" + } - val renderedTags = if (tags.isEmpty) - "" - else -s""" - tags: [${tags.map(wrap).mkString(", ")}]""" + val body = s"""name: ${wrap(job.name)}${renderedNeeds}${renderedCond}${renderedStrategy} +runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedEnv}${renderedPerm} +$renderedSteps""" - val renderedPaths = paths match { - case Paths.None => - "" - case Paths.Include(paths) => - "\n" + indent(s"""paths: [${paths.map(wrap).mkString(", ")}]""", 2) - case Paths.Ignore(paths) => - "\n" + indent(s"""paths-ignore: [${paths.map(wrap).mkString(", ")}]""", 2) - } + s"${job.id}:\n${indentOnce(body)}" + } + + def compileWorkflow(workflow: Workflow, sbt: String): String = { s"""# This file was automatically generated by sbt-github-actions using the # githubWorkflowGenerate task. You should add and commit this file to @@ -517,16 +409,9 @@ s""" # change your sbt build configuration to revise the workflow description # to meet your needs, then regenerate this file. -name: ${wrap(name)} - -on: - pull_request: - branches: [${branches.map(wrap).mkString(", ")}]$renderedTypes$renderedPaths - push: - branches: [${branches.map(wrap).mkString(", ")}]$renderedTags$renderedPaths - -${renderedPerm}${renderedEnv}jobs: -${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} +${workflow.render} +jobs: +${indentOnce(workflow.jobs.map(compileJob(_, sbt)).mkString("\n\n"))} """ } @@ -671,6 +556,9 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowGeneratedCacheSteps.value.toList }, + githubWorkflowCustomWorkflows := Map.empty, + githubWorkflowGenerationTargets := GenerationTarget.all, + githubWorkflowGeneratedCI := { val publicationCondPre = githubWorkflowPublishTargetBranches.value.map(compileBranchPredicate("github.ref", _)).mkString("(", " || ", ")") @@ -730,14 +618,25 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowSbtCommand.value } compileWorkflow( + Workflow( "Continuous Integration", - githubWorkflowTargetBranches.value.toList, - githubWorkflowTargetTags.value.toList, - githubWorkflowTargetPaths.value, - githubWorkflowPREventTypes.value.toList, - githubWorkflowPermissions.value, - githubWorkflowEnv.value, - githubWorkflowGeneratedCI.value.toList, + List( + WebhookEvent.PullRequest( + githubWorkflowTargetBranches.value.toList, + Nil, + githubWorkflowTargetPaths.value, + githubWorkflowPREventTypes.value.toList + ), + WebhookEvent.Push( + githubWorkflowTargetBranches.value.toList, + githubWorkflowTargetTags.value.toList, + githubWorkflowTargetPaths.value + ) + ), + githubWorkflowGeneratedCI.value.toList, + githubWorkflowEnv.value, + githubWorkflowPermissions.value, + ), sbt) } @@ -750,6 +649,21 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} } } + private val customWorkflowContents = Def task { + val sbt = if (githubWorkflowUseSbtThinClient.value) { + githubWorkflowSbtCommand.value + " --client" + } else { + githubWorkflowSbtCommand.value + } + + githubWorkflowCustomWorkflows.value.map { + case (file, workflow) if file.endsWith(".yml") || file.endsWith(".yaml") => + file -> compileWorkflow(workflow, sbt) + case (file, workflow) => + (file + ".yml") -> compileWorkflow(workflow, sbt) + } + } + private val workflowsDirTask = Def task { val githubDir = baseDirectory.value / ".github" val workflowsDir = githubDir / "workflows" @@ -789,13 +703,24 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} val includeClean = githubWorkflowIncludeClean.value val cleanContents = readCleanContents.value + val targets = githubWorkflowGenerationTargets.value + val workflowContents = customWorkflowContents.value + val workflowsDir = workflowsDirTask.value + val ciYml = ciYmlFile.value val cleanYml = cleanYmlFile.value - IO.write(ciYml, ciContents) + if (targets(GenerationTarget.CI)) { + IO.write(ciYml, ciContents) + } - if(includeClean) + if (targets(GenerationTarget.Clean) || includeClean) { IO.write(cleanYml, cleanContents) + } + + if (targets(GenerationTarget.Custom)) { + workflowContents.foreach { case (file, content) => IO.write(workflowsDir / file, content) } + } }, githubWorkflowCheck := { diff --git a/src/main/scala/sbtghactions/IssueCommentEventType.scala b/src/main/scala/sbtghactions/IssueCommentEventType.scala new file mode 100644 index 0000000..a004bd4 --- /dev/null +++ b/src/main/scala/sbtghactions/IssueCommentEventType.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#issue_comment + */ +sealed trait IssueCommentEventType extends EventType + +object IssueCommentEventType { + case object Created extends IssueCommentEventType + case object Edited extends IssueCommentEventType + case object Deleted extends IssueCommentEventType +} diff --git a/src/main/scala/sbtghactions/IssuesEventType.scala b/src/main/scala/sbtghactions/IssuesEventType.scala new file mode 100644 index 0000000..8bd7d69 --- /dev/null +++ b/src/main/scala/sbtghactions/IssuesEventType.scala @@ -0,0 +1,42 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#issues + */ + +sealed trait IssuesEventType extends EventType + +object IssuesEventType { + case object Opened extends IssuesEventType + case object Edited extends IssuesEventType + case object Deleted extends IssuesEventType + case object Transferred extends IssuesEventType + case object Pinned extends IssuesEventType + case object Unpinned extends IssuesEventType + case object Closed extends IssuesEventType + case object Reopened extends IssuesEventType + case object Assigned extends IssuesEventType + case object Unassigned extends IssuesEventType + case object Labeled extends IssuesEventType + case object Unlabeled extends IssuesEventType + case object Locked extends IssuesEventType + case object Unlocked extends IssuesEventType + case object Milestoned extends IssuesEventType + case object Demilestoned extends IssuesEventType +} diff --git a/src/main/scala/sbtghactions/LabelEventType.scala b/src/main/scala/sbtghactions/LabelEventType.scala new file mode 100644 index 0000000..8e710ac --- /dev/null +++ b/src/main/scala/sbtghactions/LabelEventType.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#label + */ + +sealed trait LabelEventType extends EventType + +object LabelEventType { + case object Created extends LabelEventType + case object Edited extends LabelEventType + case object Deleted extends LabelEventType +} diff --git a/src/main/scala/sbtghactions/MilestoneEventType.scala b/src/main/scala/sbtghactions/MilestoneEventType.scala new file mode 100644 index 0000000..711422b --- /dev/null +++ b/src/main/scala/sbtghactions/MilestoneEventType.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#issue_comment + */ + +sealed trait MilestoneEventType extends EventType + +object MilestoneEventType { + case object Created extends MilestoneEventType + case object Closed extends MilestoneEventType + case object Opened extends MilestoneEventType + case object Edited extends MilestoneEventType + case object Deleted extends MilestoneEventType +} diff --git a/src/main/scala/sbtghactions/PREventType.scala b/src/main/scala/sbtghactions/PREventType.scala index 79942a9..2f283f1 100644 --- a/src/main/scala/sbtghactions/PREventType.scala +++ b/src/main/scala/sbtghactions/PREventType.scala @@ -16,23 +16,27 @@ package sbtghactions -sealed trait PREventType extends Product with Serializable +/** + * @see https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request + */ + +sealed trait PREventType extends EventType object PREventType { val Defaults = List(Opened, Reopened, Synchronize) - case object Assigned extends PREventType - case object Unassigned extends PREventType - case object Labeled extends PREventType - case object Unlabeled extends PREventType - case object Opened extends PREventType - case object Edited extends PREventType - case object Closed extends PREventType - case object Reopened extends PREventType - case object Synchronize extends PREventType - case object ReadyForReview extends PREventType - case object Locked extends PREventType - case object Unlocked extends PREventType - case object ReviewRequested extends PREventType + case object Assigned extends PREventType + case object Unassigned extends PREventType + case object Labeled extends PREventType + case object Unlabeled extends PREventType + case object Opened extends PREventType + case object Edited extends PREventType + case object Closed extends PREventType + case object Reopened extends PREventType + case object Synchronize extends PREventType + case object ReadyForReview extends PREventType + case object Locked extends PREventType + case object Unlocked extends PREventType + case object ReviewRequested extends PREventType case object ReviewRequestRemoved extends PREventType } diff --git a/src/main/scala/sbtghactions/PRReviewCommentEventType.scala b/src/main/scala/sbtghactions/PRReviewCommentEventType.scala new file mode 100644 index 0000000..56bb326 --- /dev/null +++ b/src/main/scala/sbtghactions/PRReviewCommentEventType.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#issue_comment + */ + +sealed trait PRReviewCommentEventType extends EventType + +object PRReviewCommentEventType { + case object Created extends PRReviewCommentEventType + case object Edited extends PRReviewCommentEventType + case object Deleted extends PRReviewCommentEventType +} diff --git a/src/main/scala/sbtghactions/PRReviewEventType.scala b/src/main/scala/sbtghactions/PRReviewEventType.scala new file mode 100644 index 0000000..bcf9e2d --- /dev/null +++ b/src/main/scala/sbtghactions/PRReviewEventType.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2020-2021 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 + + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#pull_request_review + */ +sealed trait PRReviewEventType extends EventType + +object PRReviewEventType { + case object Submitted extends PRReviewEventType + case object Edited extends PRReviewEventType + case object Dismissed extends PRReviewEventType +} diff --git a/src/main/scala/sbtghactions/PRTargetEventType.scala b/src/main/scala/sbtghactions/PRTargetEventType.scala new file mode 100644 index 0000000..27bc30c --- /dev/null +++ b/src/main/scala/sbtghactions/PRTargetEventType.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target + */ +sealed trait PRTargetEventType extends EventType + +object PRTargetEventType { + case object Assigned extends PRTargetEventType + case object Unassigned extends PRTargetEventType + case object Labeled extends PRTargetEventType + case object Unlabeled extends PRTargetEventType + case object Opened extends PRTargetEventType + case object Edited extends PRTargetEventType + case object Closed extends PRTargetEventType + case object Reopened extends PRTargetEventType + case object Synchronize extends PRTargetEventType + case object ReadyForReview extends PRTargetEventType + case object Locked extends PRTargetEventType + case object Unlocked extends PRTargetEventType + case object ReviewRequested extends PRTargetEventType + case object ReviewRequestRemoved extends PRTargetEventType +} diff --git a/src/main/scala/sbtghactions/PermissionScope.scala b/src/main/scala/sbtghactions/PermissionScope.scala index 214628f..94bfad4 100644 --- a/src/main/scala/sbtghactions/PermissionScope.scala +++ b/src/main/scala/sbtghactions/PermissionScope.scala @@ -16,7 +16,30 @@ package sbtghactions -sealed trait Permissions extends Product with Serializable +import sbtghactions.RenderFunctions.* + +sealed trait Permissions extends Product with Serializable { + def compilePermissionsValue(permissionValue: PermissionValue): String = permissionValue match { + case PermissionValue.Read => "read" + case PermissionValue.Write => "write" + case PermissionValue.None => "none" + } + + def render: String = { + val rendered = this match { + case Permissions.ReadAll => " read-all" + case Permissions.WriteAll => " write-all" + case Permissions.None => " {}" + case Permissions.Specify(permMap) => + val map = permMap.map { + case (key, value) => + s"${key.render}: ${compilePermissionsValue(value)}" + } + "\n" + indent(map.mkString("\n"), 1) + } + s"\npermissions:$rendered" + } +} /** * @see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs#overview @@ -26,9 +49,13 @@ object Permissions { case object WriteAll extends Permissions case object None extends Permissions final case class Specify(values: Map[PermissionScope, PermissionValue]) extends Permissions + + def specify(values: (PermissionScope, PermissionValue)*): Specify = Specify(values.toMap) } -sealed trait PermissionScope extends Product with Serializable +sealed trait PermissionScope extends Product with Serializable{ + def render: String = KebabCase(productPrefix) +} object PermissionScope { case object Actions extends PermissionScope diff --git a/src/main/scala/sbtghactions/ProjectCardEventType.scala b/src/main/scala/sbtghactions/ProjectCardEventType.scala new file mode 100644 index 0000000..a877382 --- /dev/null +++ b/src/main/scala/sbtghactions/ProjectCardEventType.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#project_card + */ + +sealed trait ProjectCardEventType extends EventType + +object ProjectCardEventType { + case object Created extends ProjectCardEventType + case object Moved extends ProjectCardEventType + case object ConvertedToAnIssue extends ProjectCardEventType + case object Edited extends ProjectCardEventType + case object Deleted extends ProjectCardEventType +} diff --git a/src/main/scala/sbtghactions/ProjectColumnEventType.scala b/src/main/scala/sbtghactions/ProjectColumnEventType.scala new file mode 100644 index 0000000..b06a311 --- /dev/null +++ b/src/main/scala/sbtghactions/ProjectColumnEventType.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#project_column + */ + +sealed trait ProjectColumnEventType extends EventType + +object ProjectColumnEventType { + case object Created extends ProjectColumnEventType + case object Updated extends ProjectColumnEventType + case object Moved extends ProjectColumnEventType + case object Deleted extends ProjectColumnEventType +} diff --git a/src/main/scala/sbtghactions/ProjectEventType.scala b/src/main/scala/sbtghactions/ProjectEventType.scala new file mode 100644 index 0000000..9a0162b --- /dev/null +++ b/src/main/scala/sbtghactions/ProjectEventType.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#project + */ + +sealed trait ProjectEventType extends EventType + +object ProjectEventType { + case object Created extends ProjectEventType + case object Updated extends ProjectEventType + case object Closed extends ProjectEventType + case object Reopened extends ProjectEventType + case object Edited extends ProjectEventType + case object Deleted extends ProjectEventType +} diff --git a/src/main/scala/sbtghactions/RegistryPackageEventType.scala b/src/main/scala/sbtghactions/RegistryPackageEventType.scala new file mode 100644 index 0000000..7c737d8 --- /dev/null +++ b/src/main/scala/sbtghactions/RegistryPackageEventType.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#check_run + */ +sealed trait RegistryPackageEventType extends EventType + +object RegistryPackageEventType { + case object Published extends RegistryPackageEventType + case object Updated extends RegistryPackageEventType +} diff --git a/src/main/scala/sbtghactions/ReleaseEventType.scala b/src/main/scala/sbtghactions/ReleaseEventType.scala new file mode 100644 index 0000000..29c76a4 --- /dev/null +++ b/src/main/scala/sbtghactions/ReleaseEventType.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#release + */ +sealed trait ReleaseEventType extends EventType + +object ReleaseEventType { + case object Published extends ReleaseEventType + case object Unpublished extends ReleaseEventType + case object Created extends ReleaseEventType + case object Edited extends ReleaseEventType + case object Deleted extends ReleaseEventType + case object Prereleased extends ReleaseEventType + case object Released extends ReleaseEventType +} diff --git a/src/main/scala/sbtghactions/RenderFunctions.scala b/src/main/scala/sbtghactions/RenderFunctions.scala new file mode 100644 index 0000000..b69da89 --- /dev/null +++ b/src/main/scala/sbtghactions/RenderFunctions.scala @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2021 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 + +object RenderFunctions { + + def renderBranches(branches: Seq[String]): String = + renderParamWithList("branches", branches) + + def renderTypes(types: Seq[EventType]): String = + if (types.isEmpty) "" + else indentOnce { renderParamWithList("types", types.map(_.render)) } + + def wrap(str: String): String = + if (str.indexOf('\n') >= 0) + "|\n" + indent(str, 1) + else if (isSafeString(str)) + str + else + s"'${str.replace("'", "''")}'" + + private def isSafeString(str: String): Boolean = + !(str.indexOf(':') >= 0 || // pretend colon is illegal everywhere for simplicity + str.indexOf('#') >= 0 || // same for comment + str.indexOf('!') == 0 || + str.indexOf('*') == 0 || + str.indexOf('-') == 0 || + str.indexOf('?') == 0 || + str.indexOf('{') == 0 || + str.indexOf('}') == 0 || + str.indexOf('[') == 0 || + str.indexOf(']') == 0 || + str.indexOf(',') == 0 || + str.indexOf('|') == 0 || + str.indexOf('>') == 0 || + str.indexOf('@') == 0 || + str.indexOf('`') == 0 || + str.indexOf('"') == 0 || + str.indexOf('\'') == 0 || + str.indexOf('&') == 0) + + def indentOnce(output: String): String = indent(output, 1) + + def indent(output: String, level: Int): String = { + val space = (0 until level * 2).map(_ => ' ').mkString + val nlPrefixCount = output.takeWhile(_ == '\n').length + + if (output.isEmpty) "" + else "\n" * nlPrefixCount + (space + output.drop(nlPrefixCount).replace("\n", s"\n$space")).replaceAll("""\n[ ]+\n""", "\n\n") + } + + def renderParamWithList(paramName: String, items: Seq[String]): String = { + val rendered = items.map(wrap) + + if (rendered.isEmpty) "" + else if (rendered.map(_.length).sum < 40) rendered.mkString(s"\n$paramName: [", ", ", "]") + else rendered.map("- " + _).map(indentOnce).mkString(s"\n$paramName:\n", "\n", "\n") + } + + def renderMap(env: Map[String, String], prefix: String): String = + if (env.isEmpty) { + "" + } else { + val rendered = env map { + case (key, value) => + if (!isSafeString(key) || key.indexOf(' ') >= 0) + sys.error(s"'$key' is not a valid variable name") + + s"""$key: ${wrap(value)}""" + } + s""" +$prefix: +${indentOnce(rendered.mkString("\n"))}""" + } + + + object SnakeCase { + private val re = "[A-Z]+".r + + def apply(property: String): String = + re.replaceAllIn(property.head.toLower +: property.tail, { m => s"_${m.matched.toLowerCase}" }) + } + + object KebabCase { + private val re = "[A-Z]+".r + + def apply(property: String): String = + re.replaceAllIn(property.head.toLower +: property.tail, { m => s"-${m.matched.toLowerCase}" }) + } + +} diff --git a/src/main/scala/sbtghactions/TriggerEvent.scala b/src/main/scala/sbtghactions/TriggerEvent.scala new file mode 100644 index 0000000..6e4529b --- /dev/null +++ b/src/main/scala/sbtghactions/TriggerEvent.scala @@ -0,0 +1,184 @@ +/* + * Copyright 2020-2021 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 + +import sbtghactions.RenderFunctions._ + + +/** + * @see https://docs.github.com/en/actions/reference/events-that-trigger-workflows + */ +sealed trait TriggerEvent extends Product with Serializable { + val name: String = SnakeCase(productPrefix) + def render: String +} + +final case class Schedule(cron: String) extends TriggerEvent { + + override def render: String = + s"""|$name: + | - cron: '$cron'""".stripMargin +} + +sealed trait ManualEvent extends TriggerEvent + +object ManualEvent { + + final case class Input( + name: String, + description: String, + default: Option[String], + required: Boolean) { + + //TODO Should we check if the name is a safe string? What if not? + def render: String = + s"""|$name: + | description: ${wrap(description)} + | required: $required + |""".stripMargin + default.map(wrap).map("default: " + _).map(indentOnce).getOrElse("") + } + + final case class WorkflowDispatch(inputs: Seq[Input]) extends ManualEvent { + + override def render: String = + s"$name:${renderInputs(inputs)}" + } + + private def renderInputs(inputs: Seq[Input]) = + if (inputs.isEmpty) "" + else indentOnce { "\ninputs:\n" + inputs.map(_.render).map(indentOnce).mkString("\n") } + + final case class RepositoryDispatch(types: Seq[String]) extends ManualEvent { + + override def render: String = + s"$name:${indentOnce { renderParamWithList("types", types) }}" + } +} + +sealed trait WebhookEvent extends TriggerEvent + +sealed trait PlainNameEvent extends WebhookEvent { + final override def render: String = name +} + +sealed trait TypedEvent extends WebhookEvent { + + def types: Seq[EventType] + + final override def render: String = + s"$name:${renderTypes(types)}" +} + +object WebhookEvent { + + final case class CheckRun(types: Seq[CheckRunEventType]) extends TypedEvent + + final case class CheckSuite(types: Seq[CheckSuiteEventType]) extends TypedEvent + + case object Create extends PlainNameEvent + + case object Delete extends PlainNameEvent + + case object Deployment extends PlainNameEvent + + case object DeploymentStatus extends PlainNameEvent + + case object Fork extends PlainNameEvent + + case object Gollum extends PlainNameEvent + + final case class IssueComment(types: Seq[IssueCommentEventType]) extends TypedEvent + + final case class Issues(types: Seq[IssuesEventType]) extends TypedEvent + + final case class Label(types: Seq[LabelEventType]) extends TypedEvent + + final case class Milestone(types: Seq[MilestoneEventType]) extends TypedEvent + + case object PageBuild extends PlainNameEvent + + final case class Project(types: Seq[ProjectEventType]) extends TypedEvent + + final case class ProjectCard(types: Seq[ProjectCardEventType]) extends TypedEvent + + final case class ProjectColumn(types: Seq[ProjectColumnEventType]) extends TypedEvent + + case object Public extends PlainNameEvent + + final case class PullRequest( + branches: Seq[String], + tags: Seq[String], + paths: Paths, + types: Seq[PREventType]) + extends WebhookEvent { + + override def render: String = + s"$name:" + + indentOnce { renderBranches(branches) + renderTags + renderPaths + renderTypes } + + private def renderTags = renderParamWithList("tags", tags) + private def renderPaths: String = + paths match { + case Paths.None => "" + case Paths.Include(paths) => renderParamWithList("paths", paths) + case Paths.Ignore(paths) => renderParamWithList("paths-ignore", paths) + } + + private def renderTypes = + if (types == PREventType.Defaults) "" + else "" + renderParamWithList("types", types.map(_.render)) + + } + + final case class PullRequestReview(types: Seq[PRReviewEventType]) extends TypedEvent + + final case class PullRequestReviewComment(types: Seq[PRReviewCommentEventType]) extends TypedEvent + + final case class PullRequestTarget(types: Seq[PRTargetEventType]) extends TypedEvent + + final case class Push(branches: Seq[String], tags: Seq[String], paths: Paths) + extends WebhookEvent { + + override def render: String = + s"$name:" + + indentOnce { renderBranches(branches) + renderTags + renderPaths } + + private def renderTags: String = if (tags.isEmpty) "" else renderParamWithList("tags", tags) + private def renderPaths: String = + paths match { + case Paths.None => "" + case Paths.Include(paths) => renderParamWithList("paths", paths) + case Paths.Ignore(paths) => renderParamWithList("paths-ignore", paths) + } + + } + + final case class RegistryPackage(types: Seq[RegistryPackageEventType]) extends TypedEvent + + final case class Release(types: Seq[ReleaseEventType]) extends TypedEvent + + case object Status extends PlainNameEvent + + final case class Watch(types: Seq[WatchEventType]) extends TypedEvent + + final case class WorkflowRun(workflows: Seq[String], types: Seq[WorkflowRunEventType]) + extends WebhookEvent { + + override def render: String = + s"$name:${indentOnce(renderParamWithList("workflows", workflows))}${renderTypes(types)}" + } +} diff --git a/src/main/scala/sbtghactions/WatchEventType.scala b/src/main/scala/sbtghactions/WatchEventType.scala new file mode 100644 index 0000000..3f6e04a --- /dev/null +++ b/src/main/scala/sbtghactions/WatchEventType.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/actions/reference/events-that-trigger-workflows#watch + */ + +sealed trait WatchEventType extends EventType + +object WatchEventType { + case object Started extends WatchEventType +} diff --git a/src/main/scala/sbtghactions/Workflow.scala b/src/main/scala/sbtghactions/Workflow.scala new file mode 100644 index 0000000..9294660 --- /dev/null +++ b/src/main/scala/sbtghactions/Workflow.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2021 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 + +import sbtghactions.RenderFunctions.* + +final case class Workflow( + name: String, + ons: Seq[TriggerEvent], + jobs: Seq[WorkflowJobBase], + env: Map[String, String], + permissions: Option[Permissions], +) { + + def render: String = + s"""|name: ${wrap(name)} + | + |on:\n$renderOns$renderPermissions$renderEnv""".stripMargin + + private def renderOns = + ons.map(_.render).map(indentOnce).mkString("\n") + + private def renderPermissions = + permissions.map(_.render).mkString + + private def renderEnv: String = + renderMap(env, "env") + +} diff --git a/src/main/scala/sbtghactions/WorkflowJob.scala b/src/main/scala/sbtghactions/WorkflowJob.scala index 9bab4f5..50e00a8 100644 --- a/src/main/scala/sbtghactions/WorkflowJob.scala +++ b/src/main/scala/sbtghactions/WorkflowJob.scala @@ -16,6 +16,16 @@ package sbtghactions +sealed trait WorkflowJobBase + +final case class ReusableWorkflowJob( + id: String, + name: String, + uses: WorkflowRef, + cond: Option[String] = None, + needs: List[String] = List(), +) extends WorkflowJobBase + final case class WorkflowJob( id: String, name: String, @@ -34,4 +44,8 @@ final case class WorkflowJob( matrixExcs: List[MatrixExclude] = List(), runsOnExtraLabels: List[String] = List(), container: Option[JobContainer] = None, - environment: Option[JobEnvironment] = None) + environment: Option[JobEnvironment] = None) extends WorkflowJobBase { + + def needsJob(job: WorkflowJob): WorkflowJob = + copy(needs = needs :+ job.id) +} diff --git a/src/main/scala/sbtghactions/WorkflowRef.scala b/src/main/scala/sbtghactions/WorkflowRef.scala new file mode 100644 index 0000000..58d8512 --- /dev/null +++ b/src/main/scala/sbtghactions/WorkflowRef.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2020-2021 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 + +case class WorkflowRef( + workflowPath: String, + ref: String, + inputs: Map[String, String], + secrets: Map[String, String] +) { + lazy val render: String = + s"""uses: "$workflowPath@$ref"$renderInputs$renderSecrets""" + + private def renderInputs = RenderFunctions.renderMap(inputs, "with") + private def renderSecrets = RenderFunctions.renderMap(secrets, "secrets") +} diff --git a/src/main/scala/sbtghactions/WorkflowRunEventType.scala b/src/main/scala/sbtghactions/WorkflowRunEventType.scala new file mode 100644 index 0000000..2ec294a --- /dev/null +++ b/src/main/scala/sbtghactions/WorkflowRunEventType.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2020-2021 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 + +/** + * @see https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/webhook-events-and-payloads#workflow_run + */ +sealed trait WorkflowRunEventType extends EventType + +object WorkflowRunEventType { + case object Completed extends WorkflowRunEventType + case object Requested extends WorkflowRunEventType +} diff --git a/src/main/scala/sbtghactions/WorkflowStep.scala b/src/main/scala/sbtghactions/WorkflowStep.scala index 30efd4a..c2a2fd8 100644 --- a/src/main/scala/sbtghactions/WorkflowStep.scala +++ b/src/main/scala/sbtghactions/WorkflowStep.scala @@ -23,6 +23,13 @@ sealed trait WorkflowStep extends Product with Serializable { def name: Option[String] def cond: Option[String] def env: Map[String, String] + def params: Map[String, String] + def renderEnv: String = + RenderFunctions.renderMap(env, "env").drop(1) + + def renderParams: String = + RenderFunctions.renderMap(params, "with") + } object WorkflowStep { 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 2fa5c9b..1ef38be 100644 --- a/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml +++ b/src/sbt-test/sbtghactions/check-and-regenerate/expected-ci.yml @@ -13,10 +13,8 @@ on: push: branches: ['**'] tags: [v*] - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: build: name: Build and Test diff --git a/src/sbt-test/sbtghactions/no-clean/.github/workflows/ci.yml b/src/sbt-test/sbtghactions/no-clean/.github/workflows/ci.yml index 136ec90..5c4778a 100644 --- a/src/sbt-test/sbtghactions/no-clean/.github/workflows/ci.yml +++ b/src/sbt-test/sbtghactions/no-clean/.github/workflows/ci.yml @@ -12,10 +12,8 @@ on: branches: ['**'] push: branches: ['**'] - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: build: name: Build and Test diff --git a/src/sbt-test/sbtghactions/non-existent-target/.github/workflows/ci.yml b/src/sbt-test/sbtghactions/non-existent-target/.github/workflows/ci.yml index 166c67e..7e603d0 100644 --- a/src/sbt-test/sbtghactions/non-existent-target/.github/workflows/ci.yml +++ b/src/sbt-test/sbtghactions/non-existent-target/.github/workflows/ci.yml @@ -12,10 +12,8 @@ on: branches: ['**'] push: branches: ['**'] - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: build: name: Build and Test diff --git a/src/sbt-test/sbtghactions/sbt-native-thin-client/.github/workflows/ci.yml b/src/sbt-test/sbtghactions/sbt-native-thin-client/.github/workflows/ci.yml index ff39f1c..168fc43 100644 --- a/src/sbt-test/sbtghactions/sbt-native-thin-client/.github/workflows/ci.yml +++ b/src/sbt-test/sbtghactions/sbt-native-thin-client/.github/workflows/ci.yml @@ -12,10 +12,8 @@ on: branches: ['**'] push: branches: ['**'] - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: build: name: Build and Test diff --git a/src/sbt-test/sbtghactions/suppressed-scala-version/expected-ci.yml b/src/sbt-test/sbtghactions/suppressed-scala-version/expected-ci.yml index 4ffa035..b62103a 100644 --- a/src/sbt-test/sbtghactions/suppressed-scala-version/expected-ci.yml +++ b/src/sbt-test/sbtghactions/suppressed-scala-version/expected-ci.yml @@ -13,10 +13,8 @@ on: push: branches: ['**'] tags: [v*] - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - jobs: build: name: Build and Test diff --git a/src/test/scala/sbtghactions/GenerativePluginSpec.scala b/src/test/scala/sbtghactions/GenerativePluginSpec.scala index 1b790e2..37b7d63 100644 --- a/src/test/scala/sbtghactions/GenerativePluginSpec.scala +++ b/src/test/scala/sbtghactions/GenerativePluginSpec.scala @@ -41,12 +41,22 @@ class GenerativePluginSpec extends Specification { | branches: [main] | push: | branches: [main] - | |jobs: - |${" " * 2} + | |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.None, PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected + compileWorkflow( + Workflow( + "test", + List( + WebhookEvent.PullRequest(List("main"), Nil, Paths.None, PREventType.Defaults), + WebhookEvent.Push(List("main"), Nil, Paths.None) + ), + Nil, + Map(), + None, + ), + "sbt") mustEqual expected } "produce the appropriate skeleton around a zero-job workflow with non-empty tags" in { @@ -59,12 +69,22 @@ class GenerativePluginSpec extends Specification { | push: | branches: [main] | tags: [howdy] - | |jobs: - |${" " * 2} + | |""".stripMargin - compileWorkflow("test", List("main"), List("howdy"), Paths.None, PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected + compileWorkflow( + Workflow( + "test", + List( + WebhookEvent.PullRequest(List("main"), Nil, Paths.None, PREventType.Defaults), + WebhookEvent.Push(List("main"), List("howdy"), Paths.None) + ), + Nil, + Map(), + None, + ), + "sbt") mustEqual expected } "respect non-default pr types" in { @@ -77,12 +97,64 @@ class GenerativePluginSpec extends Specification { | types: [ready_for_review, review_requested, opened] | push: | branches: [main] - | |jobs: - |${" " * 2} + | |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.None, List(PREventType.ReadyForReview, PREventType.ReviewRequested, PREventType.Opened), None, Map(), Nil, "sbt") mustEqual expected + compileWorkflow( + Workflow( + "test", + List( + WebhookEvent.PullRequest( + List("main"), + Nil, + Paths.None, + List(PREventType.ReadyForReview, PREventType.ReviewRequested, PREventType.Opened)), + WebhookEvent.Push(List("main"), Nil, Paths.None)), + Nil, + Map(), + None, + ), + "sbt") mustEqual expected + } + + "render a job without a strategy" in { + val expected = header + """ + |name: test2 + | + |on: + | push: + | branches: [main] + |env: + | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + |jobs: + | build: + | name: Build and Test + | runs-on: [ runner-label ] + | steps: + | - run: echo Hello World + |""".stripMargin + + compileWorkflow( + Workflow( + "test2", + List(WebhookEvent.Push(List("main"), Nil, Paths.None)), + List( + WorkflowJob( + "build", + "Build and Test", + List(WorkflowStep.Run(List("echo Hello World"))), + scalas = Nil, + javas= Nil, + oses = Nil, + runsOnExtraLabels = List("runner-label") + ) + ), + Map( + "GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), + None, + ), + "sbt") mustEqual expected } "compile a one-job workflow targeting multiple branch patterns with a environment variables" in { @@ -94,13 +166,10 @@ class GenerativePluginSpec extends Specification { | branches: [main, backport/v*] | push: | branches: [main, backport/v*] - | |permissions: | id-token: write - | |env: | GITHUB_TOKEN: $${{ secrets.GITHUB_TOKEN }} - | |jobs: | build: | name: Build and Test @@ -115,21 +184,21 @@ class GenerativePluginSpec extends Specification { |""".stripMargin compileWorkflow( - "test2", - List("main", "backport/v*"), - Nil, - Paths.None, - PREventType.Defaults, - Some(Permissions.Specify(Map( - PermissionScope.IdToken -> PermissionValue.Write - ))), - Map( - "GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), - List( - WorkflowJob( - "build", - "Build and Test", - List(WorkflowStep.Run(List("echo Hello World"))))), + Workflow( + "test2", + List( + WebhookEvent.PullRequest(List("main", "backport/v*"), Nil, Paths.None, PREventType.Defaults), + WebhookEvent.Push(List("main", "backport/v*"), Nil, Paths.None) + ), + List( + WorkflowJob( + "build", + "Build and Test", + List(WorkflowStep.Run(List("echo Hello World"))))), + Map( + "GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), + Some(Permissions.specify(PermissionScope.IdToken -> PermissionValue.Write)), + ), "sbt") mustEqual expected } @@ -142,7 +211,6 @@ class GenerativePluginSpec extends Specification { | branches: [main] | push: | branches: [main] - | |jobs: | build: | name: Build and Test @@ -167,24 +235,27 @@ class GenerativePluginSpec extends Specification { | - run: whoami |""".stripMargin + compileWorkflow( - "test3", - List("main"), - Nil, - Paths.None, - PREventType.Defaults, - None, - Map(), - List( - WorkflowJob( - "build", - "Build and Test", - List(WorkflowStep.Run(List("echo yikes")))), - - WorkflowJob( - "what", - "If we just didn't", - List(WorkflowStep.Run(List("whoami"))))), + Workflow( + "test3", + List( + WebhookEvent.PullRequest(List("main"), Nil, Paths.None, PREventType.Defaults), + WebhookEvent.Push(List("main"), Nil, Paths.None) + ), + List( + WorkflowJob( + "build", + "Build and Test", + List(WorkflowStep.Run(List("echo yikes")))), + + WorkflowJob( + "what", + "If we just didn't", + List(WorkflowStep.Run(List("whoami"))))), + Map(), + None, + ), "") mustEqual expected } @@ -197,7 +268,6 @@ class GenerativePluginSpec extends Specification { | branches: [main] | push: | branches: [main] - | |jobs: | build: | name: Build and Test @@ -213,20 +283,22 @@ class GenerativePluginSpec extends Specification { |""".stripMargin compileWorkflow( - "test4", - List("main"), - Nil, - Paths.None, - PREventType.Defaults, - None, - Map(), - List( - WorkflowJob( - "build", - "Build and Test", - List(WorkflowStep.Run(List("echo yikes"))), - container = Some( - JobContainer("not:real-thing")))), + Workflow( + "test4", + List( + WebhookEvent.PullRequest(List("main"), Nil, Paths.None, PREventType.Defaults), + WebhookEvent.Push(List("main"), Nil, Paths.None) + ), + List( + WorkflowJob( + "build", + "Build and Test", + List(WorkflowStep.Run(List("echo yikes"))), + container = Some( + JobContainer("not:real-thing")))), + Map(), + None, + ), "") mustEqual expected } @@ -239,7 +311,6 @@ class GenerativePluginSpec extends Specification { | branches: [main] | push: | branches: [main] - | |jobs: | build: | name: Build and Test @@ -265,26 +336,28 @@ class GenerativePluginSpec extends Specification { |""".stripMargin compileWorkflow( - "test4", - List("main"), - Nil, - Paths.None, - PREventType.Defaults, - None, - Map(), - List( - WorkflowJob( - "build", - "Build and Test", - List(WorkflowStep.Run(List("echo yikes"))), - container = Some( - JobContainer( - "also:not-real", - credentials = Some("janedoe" -> "myvoice"), - env = Map("VERSION" -> "1.0", "PATH" -> "/nope"), - volumes = Map("/source" -> "/dest/ination"), - ports = List(80, 443), - options = List("--cpus", "1"))))), + Workflow( + "test4", + List( + WebhookEvent.PullRequest(List("main"), Nil, Paths.None, PREventType.Defaults), + WebhookEvent.Push(List("main"), Nil, Paths.None) + ), + List( + WorkflowJob( + "build", + "Build and Test", + List(WorkflowStep.Run(List("echo yikes"))), + container = Some( + JobContainer( + "also:not-real", + credentials = Some("janedoe" -> "myvoice"), + env = Map("VERSION" -> "1.0", "PATH" -> "/nope"), + volumes = Map("/source" -> "/dest/ination"), + ports = List(80, 443), + options = List("--cpus", "1"))))), + Map(), + None, + ), "") mustEqual expected } @@ -299,12 +372,31 @@ class GenerativePluginSpec extends Specification { | push: | branches: [main] | paths: ['**.scala', '**.sbt'] - | |jobs: - |${" " * 2} + | |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.Include(List("**.scala", "**.sbt")), PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected + compileWorkflow( + Workflow( + "test", + Seq( + WebhookEvent.PullRequest( + List("main"), + Nil, + Paths.Include(List("**.scala", "**.sbt")), + PREventType.Defaults + ), + WebhookEvent.Push( + List("main"), + Nil, + Paths.Include(List("**.scala", "**.sbt")) + ) + ), + Nil, + Map(), + None, + ), + "sbt") mustEqual expected } "render ignored paths on pull_request and push" in { @@ -318,12 +410,29 @@ class GenerativePluginSpec extends Specification { | push: | branches: [main] | paths-ignore: [docs/**] - | |jobs: - |${" " * 2} + | |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.Ignore(List("docs/**")), PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected + compileWorkflow( Workflow( + "test", + Seq( + WebhookEvent.PullRequest( + List("main"), + Nil, + Paths.Ignore(List("docs/**")), + PREventType.Defaults + ), + WebhookEvent.Push( + List("main"), + Nil, + Paths.Ignore(List("docs/**")) + ) + ), + Nil, + Map(), + None, + ), "sbt") mustEqual expected } } @@ -496,6 +605,31 @@ class GenerativePluginSpec extends Specification { uses: actions/checkout@v3""" } + "compile a simple job that references a reusable workflow" in { + val results = compileJob( + ReusableWorkflowJob( + "bippy", + "Bippity Bop Around the Clock", + uses = + WorkflowRef( + "some/path/.github/workflow/file.yml", + "master", + Map("git-ref" -> "${{ github.head_ref }}"), + Map("MY_SECRET" -> "${{ secrets.SECRET }}") + ) + ), + "") + + results mustEqual s"""bippy: + name: Bippity Bop Around the Clock + uses: "some/path/.github/workflow/file.yml@master" + with: + git-ref: $${{ github.head_ref }} + secrets: + MY_SECRET: $${{ secrets.SECRET }}""" + } + + "compile a job with one step and three oses" in { val results = compileJob( WorkflowJob( @@ -737,6 +871,26 @@ class GenerativePluginSpec extends Specification { - run: echo hello""" } + "compile a job with extra runs-on labels, but without oses" in { + compileJob( + WorkflowJob( + "job", + "my-name", + List( + WorkflowStep.Run(List("echo hello"))), + runsOnExtraLabels = List("runner-label", "runner-group"), + oses = Nil + ), "") mustEqual """job: + name: my-name + strategy: + matrix: + scala: [2.13.6] + java: [temurin@11] + runs-on: [ runner-label, runner-group ] + steps: + - run: echo hello""" + } + "produce an error when compiling a job with `include` key in matrix" in { compileJob( WorkflowJob( diff --git a/src/test/scala/sbtghactions/TriggerEventSpec.scala b/src/test/scala/sbtghactions/TriggerEventSpec.scala new file mode 100644 index 0000000..0d54db0 --- /dev/null +++ b/src/test/scala/sbtghactions/TriggerEventSpec.scala @@ -0,0 +1,323 @@ +/* + * Copyright 2020-2021 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 + +import org.specs2.mutable.Specification +import org.specs2.specification.AllExpectations +import sbtghactions.ManualEvent.Input + +class TriggerEventSpec extends Specification with AllExpectations { + "schedule" should { + "render cron expression" in { + Schedule("* * * * *").render mustEqual "schedule:\n - cron: '* * * * *'" + } + } + + "workflow dispatch" should { + "render without inputs" in { + ManualEvent.WorkflowDispatch(Nil).render mustEqual "workflow_dispatch:" + } + + "render inputs" in { + + val expected = + """workflow_dispatch: + | inputs: + | ref: + | description: The branch, tag or SHA to build + | required: true + | default: master""".stripMargin + + ManualEvent + .WorkflowDispatch( + List( + Input("ref", "The branch, tag or SHA to build", Some("master"), required = true) + ) + ) + .render mustEqual expected + } + } + + "repository dispatch" should { + "render without types" in { + ManualEvent.RepositoryDispatch(Nil).render mustEqual "repository_dispatch:" + } + + "render types" in { + + val expected = + """repository_dispatch: + | types: [event1, event2]""".stripMargin + + ManualEvent.RepositoryDispatch(List("event1", "event2")).render mustEqual expected + } + } + + "pull request" should { + "render without branches, tags, paths or types" in { + val expected = "pull_request:" + WebhookEvent.PullRequest(Nil, Nil, Paths.None, Nil).render mustEqual expected + } + + "render without branches, tags, paths, but with types" in { + val expected = + """|pull_request: + | types: [edited, ready_for_review]""".stripMargin + WebhookEvent + .PullRequest(Nil, Nil, Paths.None, List(PREventType.Edited, PREventType.ReadyForReview)) + .render mustEqual expected + } + + "render without branches, but with tags, paths and types" in { + val expected = + """|pull_request: + | tags: [v1*, v2*] + | paths: [src/main/**] + | types: [edited, ready_for_review]""".stripMargin + WebhookEvent + .PullRequest( + Nil, + List("v1*", "v2*"), + Paths.Include(List("src/main/**")), + List(PREventType.Edited, PREventType.ReadyForReview) + ) + .render mustEqual expected + } + + "render with branches, tags and types" in { + val expected = + """|pull_request: + | branches: [master] + | tags: [v1*, v2*] + | paths: [src/main/**] + | types: [edited, ready_for_review]""".stripMargin + WebhookEvent + .PullRequest( + List("master"), + List("v1*", "v2*"), + Paths.Include(List("src/main/**")), + List(PREventType.Edited, PREventType.ReadyForReview) + ) + .render mustEqual expected + } + + "render without types, but with branches, tags and paths" in { + val expected = + """|pull_request: + | branches: [master] + | tags: [v1*, v2*] + | paths: [src/main/**]""".stripMargin + WebhookEvent + .PullRequest(List("master"), List("v1*", "v2*"), Paths.Include(List("src/main/**")), Nil) + .render mustEqual expected + } + + "render without tags, but with branches and types" in { + val expected = + """|pull_request: + | branches: [master] + | types: [edited, ready_for_review]""".stripMargin + WebhookEvent + .PullRequest(List("master"), Nil, Paths.None, List(PREventType.Edited, PREventType.ReadyForReview)) + .render mustEqual expected + } + "render only with paths" in { + val expected = + """|pull_request: + | paths: [src/main/**]""".stripMargin + WebhookEvent.PullRequest(Nil, Nil, Paths.Include(List("src/main/**")), Nil).render mustEqual expected + } + } + + "push" should { + "render without branches, tags or paths" in { + val expected = "push:" + WebhookEvent.Push(Nil, Nil, Paths.None).render mustEqual expected + } + + "render without branches, but with tags and paths" in { + val expected = + """|push: + | tags: [v1*, v2*] + | paths: [src/main/**]""".stripMargin + WebhookEvent.Push(Nil, List("v1*", "v2*"), Paths.Include(List("src/main/**"))).render mustEqual expected + } + + "render only with paths" in { + val expected = + """|push: + | paths: [src/main/**]""".stripMargin + WebhookEvent.Push(Nil, Nil, Paths.Include(List("src/main/**"))).render mustEqual expected + } + + "render with branches and tags" in { + val expected = + """|push: + | branches: [master] + | tags: [v1*, v2*]""".stripMargin + WebhookEvent.Push(List("master"), List("v1*", "v2*"), Paths.None).render mustEqual expected + } + + "render without tags, but with branches" in { + val expected = + """|push: + | branches: [master]""".stripMargin + WebhookEvent.Push(List("master"), Nil, Paths.None).render mustEqual expected + } + } + + "workflow run" should { + "render without workflows and types" in { + val expected = "workflow_run:" + WebhookEvent.WorkflowRun(Nil, Nil).render mustEqual expected + } + } + + "plain name events" should { + "render their name only" in { + + val nameEvents = List( + (WebhookEvent.Create, "create"), + (WebhookEvent.Delete, "delete"), + (WebhookEvent.Deployment, "deployment"), + (WebhookEvent.DeploymentStatus, "deployment_status"), + (WebhookEvent.Fork, "fork"), + (WebhookEvent.Gollum, "gollum"), + (WebhookEvent.PageBuild, "page_build"), + (WebhookEvent.Public, "public"), + (WebhookEvent.Status, "status"), + ) + + forall(nameEvents) { case (event, name) => + event.render mustEqual s"$name" + } + + } + } + + "typed events" should { + "render their name only, if no types are given" in { + + val events = List( + (WebhookEvent.CheckRun(Nil), "check_run:"), + (WebhookEvent.CheckSuite(Nil), "check_suite:"), + (WebhookEvent.IssueComment(Nil), "issue_comment:"), + (WebhookEvent.Issues(Nil), "issues:"), + (WebhookEvent.Label(Nil), "label:"), + (WebhookEvent.Milestone(Nil), "milestone:"), + (WebhookEvent.Project(Nil), "project:"), + (WebhookEvent.ProjectCard(Nil), "project_card:"), + (WebhookEvent.ProjectColumn(Nil), "project_column:"), + (WebhookEvent.PullRequestReview(Nil), "pull_request_review:"), + (WebhookEvent.PullRequestReviewComment(Nil), "pull_request_review_comment:"), + (WebhookEvent.PullRequestTarget(Nil), "pull_request_target:"), + (WebhookEvent.RegistryPackage(Nil), "registry_package:"), + (WebhookEvent.Release(Nil), "release:"), + (WebhookEvent.Watch(Nil), "watch:"), + ) + + forall(events) { case (event, rendered) => + event.render mustEqual s"$rendered" + } + + } + + "render their name only, if no types are given" in { + + val events = List( + ( + WebhookEvent.CheckRun(Seq(CheckRunEventType.Created, CheckRunEventType.Completed)), + "check_run:\n types: [created, completed]" + ), + ( + WebhookEvent.CheckSuite( + Seq(CheckSuiteEventType.Requested, CheckSuiteEventType.Completed) + ), + "check_suite:\n types: [requested, completed]" + ), + ( + WebhookEvent.IssueComment( + Seq(IssueCommentEventType.Created, IssueCommentEventType.Edited) + ), + "issue_comment:\n types: [created, edited]" + ), + ( + WebhookEvent.Issues(Seq(IssuesEventType.Opened, IssuesEventType.Edited)), + "issues:\n types: [opened, edited]" + ), + ( + WebhookEvent.Label(Seq(LabelEventType.Created, LabelEventType.Edited)), + "label:\n types: [created, edited]" + ), + ( + WebhookEvent.Milestone(Seq(MilestoneEventType.Opened, MilestoneEventType.Closed)), + "milestone:\n types: [opened, closed]" + ), + ( + WebhookEvent.Project(Seq(ProjectEventType.Created, ProjectEventType.Closed)), + "project:\n types: [created, closed]" + ), + ( + WebhookEvent.ProjectCard( + Seq(ProjectCardEventType.ConvertedToAnIssue, ProjectCardEventType.Moved) + ), + "project_card:\n types: [converted_to_an_issue, moved]" + ), + ( + WebhookEvent.ProjectColumn( + Seq(ProjectColumnEventType.Moved, ProjectColumnEventType.Created) + ), + "project_column:\n types: [moved, created]" + ), + ( + WebhookEvent.PullRequestReview( + Seq(PRReviewEventType.Edited, PRReviewEventType.Dismissed) + ), + "pull_request_review:\n types: [edited, dismissed]" + ), + ( + WebhookEvent.PullRequestReviewComment( + Seq(PRReviewCommentEventType.Created, PRReviewCommentEventType.Edited) + ), + "pull_request_review_comment:\n types: [created, edited]" + ), + ( + WebhookEvent.PullRequestTarget( + Seq(PRTargetEventType.Edited, PRTargetEventType.ReadyForReview) + ), + "pull_request_target:\n types: [edited, ready_for_review]" + ), + ( + WebhookEvent.RegistryPackage( + Seq(RegistryPackageEventType.Updated, RegistryPackageEventType.Published) + ), + "registry_package:\n types: [updated, published]" + ), + ( + WebhookEvent.Release(Seq(ReleaseEventType.Edited, ReleaseEventType.Unpublished)), + "release:\n types: [edited, unpublished]" + ), + (WebhookEvent.Watch(Seq(WatchEventType.Started)), "watch:\n types: [started]"), + ) + + forall(events) { case (event, rendered) => + event.render mustEqual s"$rendered" + } + + } + } +}