From 546d028509b4358b9550d31d7b4bc4d11c0aa146 Mon Sep 17 00:00:00 2001 From: Darren Gibson Date: Wed, 12 Feb 2025 14:39:38 -0600 Subject: [PATCH] Create a `Workflow` type to capture an entire workflow file. Split out re-usable workflows for regular workflows Add needs Bin friendlier Triggers, added Workflow Call Add permissions and concurrency. Refactor needs Cleanup render of types and steps Cleaned up redundant empty string checks, added prefix, suffix to some compile matrix Add more trigger types --- .github/workflows/ci.yml | 3 +- .github/workflows/clean.yml | 8 +- .../org/typelevel/sbt/TypelevelCiPlugin.scala | 4 +- github-actions/src/main/resources/clean.yml | 59 -- .../org/typelevel/sbt/gha/Concurrency.scala | 15 +- .../typelevel/sbt/gha/GenerativeKeys.scala | 8 + .../typelevel/sbt/gha/GenerativePlugin.scala | 646 +++++++++++------- .../scala/org/typelevel/sbt/gha/Secrets.scala | 25 + .../org/typelevel/sbt/gha/Workflow.scala | 90 +++ .../org/typelevel/sbt/gha/WorkflowJob.scala | 384 ++++++++--- .../typelevel/sbt/gha/WorkflowTrigger.scala | 330 +++++++++ .../typelevel/sbt/mergify/MergifyPlugin.scala | 4 +- .../typelevel/sbt/TypelevelSitePlugin.scala | 2 +- 13 files changed, 1140 insertions(+), 438 deletions(-) delete mode 100644 github-actions/src/main/resources/clean.yml create mode 100644 github-actions/src/main/scala/org/typelevel/sbt/gha/Secrets.scala create mode 100644 github-actions/src/main/scala/org/typelevel/sbt/gha/Workflow.scala create mode 100644 github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowTrigger.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa5cc1a5..00a91aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ name: Continuous Integration -on: +on: pull_request: branches: ['**', '!update/**', '!pr/**'] push: @@ -31,7 +31,6 @@ permissions: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - concurrency: group: ${{ github.workflow }} @ ${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml index 547aaa43..ebe0745e 100644 --- a/.github/workflows/clean.yml +++ b/.github/workflows/clean.yml @@ -7,12 +7,16 @@ name: Clean -on: push +on: + push: jobs: delete-artifacts: name: Delete Artifacts - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-22.04] + runs-on: ${{ matrix.os }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/ci/src/main/scala/org/typelevel/sbt/TypelevelCiPlugin.scala b/ci/src/main/scala/org/typelevel/sbt/TypelevelCiPlugin.scala index 877fa5f3..6e1e6629 100644 --- a/ci/src/main/scala/org/typelevel/sbt/TypelevelCiPlugin.scala +++ b/ci/src/main/scala/org/typelevel/sbt/TypelevelCiPlugin.scala @@ -150,7 +150,7 @@ object TypelevelCiPlugin extends AutoPlugin { val dependencySubmission = if (tlCiDependencyGraphJob.value) List( - WorkflowJob( + WorkflowJob.Run( "dependency-submission", "Submit Dependencies", scalas = Nil, @@ -173,7 +173,7 @@ object TypelevelCiPlugin extends AutoPlugin { Some(file(".scala-steward.conf")).filter(_.exists()), githubWorkflowAddedJobs ++= { tlCiStewardValidateConfig.value.toList.map { config => - WorkflowJob( + WorkflowJob.Run( "validate-steward", "Validate Steward Config", WorkflowStep.Checkout :: diff --git a/github-actions/src/main/resources/clean.yml b/github-actions/src/main/resources/clean.yml deleted file mode 100644 index 547aaa43..00000000 --- a/github-actions/src/main/resources/clean.yml +++ /dev/null @@ -1,59 +0,0 @@ -# This file was automatically generated by sbt-github-actions using the -# githubWorkflowGenerate task. You should add and commit this file to -# your git repository. It goes without saying that you shouldn't edit -# this file by hand! Instead, if you wish to make changes, you should -# change your sbt build configuration to revise the workflow description -# to meet your needs, then regenerate this file. - -name: Clean - -on: push - -jobs: - delete-artifacts: - name: Delete Artifacts - runs-on: ubuntu-latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Delete artifacts - run: | - # Customize those three lines with your repository and credentials: - REPO=${GITHUB_API_URL}/repos/${{ github.repository }} - - # A shortcut to call GitHub API. - ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } - - # A temporary file which receives HTTP response headers. - TMPFILE=/tmp/tmp.$$ - - # An associative array, key: artifact name, value: number of artifacts of that name. - declare -A ARTCOUNT - - # Process all artifacts on this repository, loop on returned "pages". - URL=$REPO/actions/artifacts - while [[ -n "$URL" ]]; do - - # Get current page, get response headers in a temporary file. - JSON=$(ghapi --dump-header $TMPFILE "$URL") - - # Get URL of next page. Will be empty if we are at the last page. - URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') - rm -f $TMPFILE - - # Number of artifacts on this page: - COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) - - # Loop on all artifacts on this page. - for ((i=0; $i < $COUNT; i++)); do - - # Get name of artifact and count instances of this name. - name=$(jq <<<$JSON -r ".artifacts[$i].name?") - ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) - - id=$(jq <<<$JSON -r ".artifacts[$i].id?") - size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) - printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size - ghapi -X DELETE $REPO/actions/artifacts/$id - done - done diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/Concurrency.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/Concurrency.scala index db56a8e5..690fbc12 100644 --- a/github-actions/src/main/scala/org/typelevel/sbt/gha/Concurrency.scala +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/Concurrency.scala @@ -18,15 +18,24 @@ package org.typelevel.sbt.gha sealed abstract class Concurrency { def group: String - def cancelInProgress: Option[Boolean] + def cancelInProgress: Option[String] } object Concurrency { + def apply(group: String): Concurrency = + Impl(group, None) - def apply(group: String, cancelInProgress: Option[Boolean] = None): Concurrency = + def apply(group: String, cancelInProgress: Boolean): Concurrency = + apply(group, Some(cancelInProgress)) + + def apply(group: String, cancelInProgress: Option[Boolean]): Concurrency = + Impl(group, cancelInProgress.map(_.toString)) + + def apply(group: String, cancelInProgress: Option[String])( + implicit dummy: DummyImplicit): Concurrency = Impl(group, cancelInProgress) - private final case class Impl(group: String, cancelInProgress: Option[Boolean]) + private final case class Impl(group: String, cancelInProgress: Option[String]) extends Concurrency { override def productPrefix = "Concurrency" } diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativeKeys.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativeKeys.scala index 3ddcfc3d..0423d7ae 100644 --- a/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativeKeys.scala +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativeKeys.scala @@ -25,6 +25,10 @@ trait GenerativeKeys { 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 githubWorkflows = settingKey[Map[String, Workflow]]( + "The map of jobs which will make up the generated workflows, with the keys being the workflow file path.") + lazy val githubWorkflowCI = + settingKey[Workflow]("The Workflow which will make up the generated ci workflow (ci.yml)") 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]]( @@ -65,6 +69,8 @@ trait GenerativeKeys { s"Commands automatically prepended to a WorkflowStep.Sbt (default: ['++ $${{ matrix.scala }}'])") lazy val githubWorkflowBuild = settingKey[Seq[WorkflowStep]]( "A sequence of workflow steps which compile and test the project (default: [Sbt(List(\"test\"))])") + lazy val githubWorkflowBuildJob = + settingKey[WorkflowJob]("A workflow job for compiling and testing the project") lazy val githubWorkflowPublishPreamble = settingKey[Seq[WorkflowStep]]( "A list of steps to insert after base setup but before publishing (default: [])") @@ -72,6 +78,8 @@ trait GenerativeKeys { "A list of steps to insert after publication but before the end of the publish job (default: [])") lazy val githubWorkflowPublish = settingKey[Seq[WorkflowStep]]( "A sequence workflow steps which publishes the project (default: [Sbt(List(\"+publish\"))])") + lazy val githubWorkflowPublishJob = + settingKey[WorkflowJob]("A workflow job which publishes the project.") lazy val githubWorkflowPublishTargetBranches = settingKey[Seq[RefPredicate]]( "A set of branch predicates which will be applied to determine whether the current branch gets a publication stage; if empty, publish will be skipped entirely (default: [== main])") lazy val githubWorkflowPublishCond = settingKey[Option[String]]( diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativePlugin.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativePlugin.scala index fccb35bf..9fa8678e 100644 --- a/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativePlugin.scala +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/GenerativePlugin.scala @@ -16,11 +16,12 @@ package org.typelevel.sbt.gha +import org.typelevel.sbt.gha.WorkflowTrigger.BranchesFilter +import org.typelevel.sbt.gha.WorkflowTrigger.TagsFilter import sbt.Keys._ import sbt._ import java.nio.file.FileSystems -import scala.io.Source object GenerativePlugin extends AutoPlugin { @@ -80,7 +81,7 @@ object GenerativePlugin extends AutoPlugin { 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") + output.replaceAll("(?m)^", space).replaceAll("""\n[ ]+\n""", "\n\n") } private def isSafeString(str: String): Boolean = @@ -92,8 +93,8 @@ object GenerativePlugin extends AutoPlugin { 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 || @@ -111,6 +112,93 @@ object GenerativePlugin extends AutoPlugin { else s"'${str.replace("'", "''")}'" + def compileOn(on: List[WorkflowTrigger]): String = { + def renderList(field: String, values: List[String]): String = + s"$field:${compileList(values, 1)}\n" + def renderBranchesFilter(filter: Option[BranchesFilter]) = + filter.fold("") { + case BranchesFilter.Branches(branches) if branches.size != 0 => + renderList("branches", branches) + case BranchesFilter.BranchesIgnore(branches) if branches.size != 0 => + renderList("branches-ignore", branches) + case _ => "" + } + def renderTypes(prEventTypes: List[PREventType]) = + if (prEventTypes.sortBy(_.toString) == PREventType.Defaults) "" + else renderList("types", prEventTypes.map(compilePREventType)) + def renderTagsFilter(filter: Option[TagsFilter]) = + filter.fold("") { + case TagsFilter.Tags(tags) if tags.size != 0 => + renderList("tags", tags) + case TagsFilter.TagsIgnore(tags) if tags.size != 0 => + renderList("tags-ignore", tags) + case _ => "" + } + def renderPaths(paths: Paths) = paths match { + case Paths.None => "" + case Paths.Include(paths) => renderList("paths", paths) + case Paths.Ignore(paths) => renderList("paths-ignore", paths) + } + + import WorkflowTrigger._ + val renderedTriggers = + on.map { + case pr: WorkflowTrigger.PullRequest => + val renderedBranches = renderBranchesFilter(pr.branchesFilter) + val renderedTypes = renderTypes(pr.types) + val renderedPaths = renderPaths(pr.paths) + val compose = renderedBranches + renderedTypes + renderedPaths + "pull_request:\n" + indent(compose, 1) + case push: WorkflowTrigger.Push => + val renderedBranchesFilter = renderBranchesFilter(push.branchesFilter) + val renderedTagsFilter = renderTagsFilter(push.tagsFilter) + val renderedPaths = renderPaths(push.paths) + val compose = renderedBranchesFilter + renderedTagsFilter + renderedPaths + "push:\n" + indent(compose, 1) + case call: WorkflowTrigger.WorkflowCall => + if (call.inputs.size == 0) "workflow_call:\n" + else { + val renderedInputs = { + def renderInput(id: String, i: WorkflowTrigger.WorkflowCallInput): String = { + val rndrType = i.`type` match { + case WorkflowCallInputType.Boolean => "type: boolean\n" + case WorkflowCallInputType.Number => "type: number\n" + case WorkflowCallInputType.String => "type: string\n" + } + val rndrDescription = i.description.fold("")(d => s"description: $d\n") + val rndrRequired = s"required: ${i.required}\n" + val rndrDefault = i.default.fold("")(d => s"default: $d\n") + s"$id:\n" + indent(rndrDescription + rndrRequired + rndrDefault + rndrType, 1) + } + "inputs:\n" + indent(call.inputs.map(renderInput _ tupled).mkString(""), 1) + } + "workflow_call:\n" + indent(renderedInputs, 1) + } + case dispatch: WorkflowTrigger.WorkflowDispatch => + val renderedInputs = { + def renderInput(id: String, i: WorkflowTrigger.WorkflowDispatchInput): String = { + val rndrType = i.`type` match { + case WorkflowDispatchInputType.Boolean => "type: boolean\n" + case WorkflowDispatchInputType.Number => "type: number\n" + case WorkflowDispatchInputType.String => "type: string\n" + case WorkflowDispatchInputType.Environment => "type: environment\n" + case WorkflowDispatchInputType.Choice(options) => + "type: choice\n" + indent(options.mkString("- ", "\n- ", "\n"), 1) + } + val rndrDescription = i.description.fold("")(d => s"description: $d\n") + val rndrRequired = s"required: ${i.required}\n" + val rndrDefault = i.default.fold("")(d => s"default: $d\n") + s"$id:\n" + indent(rndrDescription + rndrRequired + rndrDefault + rndrType, 1) + } + "inputs:\n" + indent(dispatch.inputs.map(renderInput _ tupled).mkString("\n"), 1) + } + "workflow_dispatch:\n" + indent(renderedInputs, 1) + case raw: WorkflowTrigger.Raw => raw.toYaml + }.mkString("\n", "", "") + + "on:" + indent(renderedTriggers, 1) + } + def compileList(items: List[String], level: Int): String = { val rendered = items.map(wrap) if (rendered.map(_.length).sum < 40) // just arbitrarily... @@ -147,6 +235,11 @@ object GenerativePlugin extends AutoPlugin { } } + def compileSecrets(secrets: Secrets): String = secrets match { + case Secrets.Inherit => s"\nsecrets: inherit" + case Secrets.Values(values) => compileMap(values, prefix = "\nsecrets") + } + def compileRef(ref: Ref): String = ref match { case Ref.Branch(name) => s"refs/heads/$name" case Ref.Tag(name) => s"refs/tags/$name" @@ -176,7 +269,7 @@ object GenerativePlugin extends AutoPlugin { concurrency.cancelInProgress match { case Some(value) => val fields = s"""group: ${wrap(concurrency.group)} - |cancel-in-progress: ${wrap(value.toString)}""".stripMargin + |cancel-in-progress: ${wrap(value)}""".stripMargin s"""concurrency: |${indent(fields, 1)}""".stripMargin @@ -195,19 +288,21 @@ 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 compileEnv(env: Map[String, String], prefix: String = "", suffix: String = ""): String = + compileMap(env, prefix = s"${prefix}env", suffix = suffix) + def compileMap(data: Map[String, String], prefix: String = "", suffix: String = ""): String = + if (data.isEmpty) "" + else { + val rendered = data + .map { + case (key, value) => + if (!isSafeString(key) || key.indexOf(' ') >= 0) + sys.error(s"'$key' is not a valid variable name") + + s"""$key: ${wrap(value)}""" + } + .mkString("\n") + s"""$prefix:\n${indent(rendered, 1)}$suffix""" } def compilePermissionScope(permissionScope: PermissionScope): String = permissionScope match { @@ -233,7 +328,10 @@ ${indent(rendered.mkString("\n"), 1)}""" case PermissionValue.None => "none" } - def compilePermissions(permissions: Option[Permissions]): String = { + def compilePermissions( + permissions: Option[Permissions], + prefix: String = "", + suffix: String = ""): String = { permissions match { case Some(perms) => val rendered = perms match { @@ -247,7 +345,7 @@ ${indent(rendered.mkString("\n"), 1)}""" } "\n" + indent(map.mkString("\n"), 1) } - s"permissions:$rendered" + s"${prefix}permissions:$rendered$suffix" case None => "" } @@ -266,25 +364,14 @@ ${indent(rendered.mkString("\n"), 1)}""" val renderedShell = if (declareShell) "shell: bash\n" else "" val renderedContinueOnError = if (step.continueOnError) "continue-on-error: true\n" else "" - val renderedEnvPre = compileEnv(step.env) - val renderedEnv = - if (renderedEnvPre.isEmpty) - "" - else - renderedEnvPre + "\n" + val renderedEnv = compileEnv(step.env, suffix = "\n") val renderedTimeoutMinutes = step.timeoutMinutes.map("timeout-minutes: " + _ + "\n").getOrElse("") - val preamblePre = + val preamble: String = renderedName + renderedId + renderedCond + renderedEnv + renderedTimeoutMinutes - val preamble = - if (preamblePre.isEmpty) - "" - else - preamblePre - val body = step match { case run: Run => val renderedWorkingDirectory = @@ -368,23 +455,45 @@ ${indent(rendered.mkString("\n"), 1)}""" renderedShell + renderedWorkingDirectory + renderedContinueOnError + "run: " + wrap( commands.mkString("\n")) + renderParams(params) - def renderParams(params: Map[String, String]): String = { - val renderedParamsPre = compileEnv(params, prefix = "with") - val renderedParams = - if (renderedParamsPre.isEmpty) + def renderParams(params: Map[String, String]): String = + compileMap(params, prefix = "\nwith") + + def compileJob(job: WorkflowJob, sbt: String): String = job match { + case job: WorkflowJob.Run => compileRunJob(job, sbt) + case job: WorkflowJob.Use => compileUseJob(job) + } + def compileUseJob(job: WorkflowJob.Use): String = { + val renderedNeeds = + if (job.needs.isEmpty) "" else - "\n" + renderedParamsPre + job.needs.mkString("\nneeds: [", ", ", "]") + + val renderedConcurrency = + job.concurrency.map(compileConcurrency).map("\n" + _).getOrElse("") + + val renderedPermissions = compilePermissions(job.permissions, prefix = "\n") + val renderedSecrets = job.secrets.fold("")(compileSecrets) + + val renderedOutputs = compileMap(job.outputs, prefix = "\noutputs") + + val renderedInputs = compileMap(job.params, prefix = "\nwith") + + // format: off + val body = s"""name: ${wrap(job.name)}${renderedNeeds}${renderedConcurrency} + |uses: ${job.uses}${renderedInputs}${renderedOutputs}${renderedSecrets}${renderedPermissions} + |""".stripMargin + // format: on - renderedParams + s"${job.id}:\n${indent(body, 1)}" } - def compileJob(job: WorkflowJob, sbt: String): String = { + def compileRunJob(job: WorkflowJob.Run, sbt: String): String = { val renderedNeeds = if (job.needs.isEmpty) "" else - s"\nneeds: [${job.needs.mkString(", ")}]" + job.needs.mkString("\nneeds: [", ", ", "]") val renderedEnvironment = job.environment.map(compileEnvironment).map("\n" + _).getOrElse("") @@ -397,7 +506,7 @@ ${indent(rendered.mkString("\n"), 1)}""" val renderedContainer = job.container match { case Some(JobContainer(image, credentials, env, volumes, ports, options)) => if (credentials.isEmpty && env.isEmpty && volumes.isEmpty && ports.isEmpty && options.isEmpty) { - "\n" + s"container: ${wrap(image)}" + s"\ncontainer: ${wrap(image)}" } else { val renderedImage = s"image: ${wrap(image)}" @@ -409,11 +518,7 @@ ${indent(rendered.mkString("\n"), 1)}""" "" } - val renderedEnv = - if (env.nonEmpty) - "\n" + compileEnv(env) - else - "" + val renderedEnv = compileEnv(env, prefix = "\n") val renderedVolumes = if (volumes.nonEmpty) @@ -440,32 +545,30 @@ ${indent(rendered.mkString("\n"), 1)}""" "" } - val renderedEnvPre = compileEnv(job.env) - val renderedEnv = - if (renderedEnvPre.isEmpty) - "" - else - "\n" + renderedEnvPre + val renderedEnv = compileEnv(job.env, "\n") - val renderedPermPre = compilePermissions(job.permissions) - val renderedPerm = - if (renderedPermPre.isEmpty) - "" - else - "\n" + renderedPermPre + val renderedOutputs = compileMap(job.outputs, prefix = "\noutputs") + + val renderedPerm = compilePermissions(job.permissions, prefix = "\n") val renderedTimeoutMinutes = job.timeoutMinutes.map(timeout => s"\ntimeout-minutes: $timeout").getOrElse("") - List("include", "exclude") foreach { key => + 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.toList.sortBy(_._1) map { - case (key, values) => s"$key: ${values.map(wrap).mkString("[", ", ", "]")}" - } mkString "\n" + val renderedMatricesAdds = + if (job.matrixAdds.isEmpty) "" + else + job + .matrixAdds + .toList + .sortBy(_._1) + .map { case (key, values) => s"$key: ${values.map(wrap).mkString("[", ", ", "]")}" } + .mkString("\n", "\n", "") // TODO refactor all of this stuff to use whitelist instead val whitelist = Map( @@ -487,44 +590,35 @@ ${indent(rendered.mkString("\n"), 1)}""" } } - 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" + val renderedIncludes = + if (job.matrixIncs.isEmpty) "" + else { + job.matrixIncs.foreach(inc => checkMatching(inc.matching)) - s"${renderedMatrices}include:\n${indent(rendered, 1)}" - } + val rendered = compileListOfSimpleDicts( + job.matrixIncs.map(i => i.matching ++ i.additions)) - val renderedExcludesPre = if (job.matrixExcs.isEmpty) { - renderedIncludesPre - } else { - job.matrixExcs.foreach(exc => checkMatching(exc.matching)) + s"\ninclude:\n${indent(rendered, 1)}" + } - val rendered = compileListOfSimpleDicts(job.matrixExcs.map(_.matching)) + val renderedExcludes = + if (job.matrixExcs.isEmpty) "" + else { + job.matrixExcs.foreach(exc => checkMatching(exc.matching)) - val renderedIncludes = - if (renderedIncludesPre.isEmpty) - "" - else - renderedIncludesPre + "\n" + val rendered = compileListOfSimpleDicts(job.matrixExcs.map(_.matching)) - s"${renderedIncludes}exclude:\n${indent(rendered, 1)}" - } + s"\nexclude:\n${indent(rendered, 1)}" + } - val renderedMatrices = - if (renderedExcludesPre.isEmpty) - "" - else - "\n" + indent(renderedExcludesPre, 2) + val renderedMatrices = indent( + buildMatrix( + 0, + "os" -> job.oses, + "scala" -> job.scalas, + "java" -> job.javas.map(_.render)) + + renderedMatricesAdds + renderedIncludes + renderedExcludes, + 2) val declareShell = job.oses.exists(_.contains("windows")) @@ -536,14 +630,20 @@ ${indent(rendered.mkString("\n"), 1)}""" val renderedFailFast = job.matrixFailFast.fold("")("\n fail-fast: " + _) + val renderedSteps = indent( + job + .steps + .map(compileStep(_, sbt, job.sbtStepPreamble, declareShell = declareShell)) + .mkString("\n\n"), + 1) // format: off val body = s"""name: ${wrap(job.name)}${renderedNeeds}${renderedCond} -strategy:${renderedFailFast} - matrix: -${buildMatrix(2, "os" -> job.oses, "scala" -> job.scalas, "java" -> job.javas.map(_.render))}${renderedMatrices} -runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedPerm}${renderedEnv}${renderedConcurrency}${renderedTimeoutMinutes} -steps: -${indent(job.steps.map(compileStep(_, sbt, job.sbtStepPreamble, declareShell = declareShell)).mkString("\n\n"), 1)}""" + |strategy:${renderedFailFast} + | matrix: + |${renderedMatrices} + |runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedPerm}${renderedEnv}${renderedOutputs}${renderedConcurrency}${renderedTimeoutMinutes} + |steps: + |${renderedSteps}""".stripMargin // format: on s"${job.id}:\n${indent(body, 1)}" @@ -558,7 +658,7 @@ ${indent(job.steps.map(compileStep(_, sbt, job.sbtStepPreamble, declareShell = d .map(indent(_, level)) .mkString("\n") - def compileWorkflow( + private def toWorkflow( name: String, branches: List[String], tags: List[String], @@ -567,66 +667,56 @@ ${indent(job.steps.map(compileStep(_, sbt, job.sbtStepPreamble, declareShell = d permissions: Option[Permissions], env: Map[String, String], concurrency: Option[Concurrency], - jobs: List[WorkflowJob], - sbt: String): String = { + jobs: List[WorkflowJob] + ): Workflow = { + Workflow( + on = List( + WorkflowTrigger.PullRequest( + branchesFilter = + if (branches.isEmpty) None else Some(BranchesFilter.Branches(branches)), + paths = paths, + types = prEventTypes), + WorkflowTrigger.Push( + branchesFilter = + if (branches.isEmpty) None else Some(BranchesFilter.Branches(branches)), + tagsFilter = if (tags.isEmpty) None else Some(TagsFilter.Tags(tags)), + paths = paths + ) + ) + ).withName(Option(name)) + .withPermissions(permissions) + .withEnv(env) + .withConcurrency(concurrency) + .withJobs(jobs) + } - 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" + def render(workflow: Workflow, sbt: String): String = { + import workflow._ - val renderedConcurrency = - concurrency.map(compileConcurrency).map("\n" + _ + "\n\n").getOrElse("") + val renderedName = name.fold("") { name => s"name: ${wrap(name)}" } - val renderedTypesPre = prEventTypes.map(compilePREventType).mkString("[", ", ", "]") - val renderedTypes = - if (prEventTypes.sortBy(_.toString) == PREventType.Defaults) - "" - else - "\n" + indent("types: " + renderedTypesPre, 2) + val renderedEnv = compileEnv(env, suffix = "\n\n") + val renderedPerm = compilePermissions(permissions, suffix = "\n\n") - val renderedTags = - if (tags.isEmpty) - "" - else - s""" - tags: [${tags.map(wrap).mkString(", ")}]""" + val renderedConcurrency = + concurrency.map(compileConcurrency).map(_ + "\n\n").getOrElse("") - 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) - } + val renderedOn = compileOn(on) + + val renderedJobs = "jobs:\n" + indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1) s"""# This file was automatically generated by sbt-github-actions using the -# githubWorkflowGenerate task. You should add and commit this file to -# your git repository. It goes without saying that you shouldn't edit -# this file by hand! Instead, if you wish to make changes, you should -# 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}${renderedConcurrency}jobs: -${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} -""" + |# githubWorkflowGenerate task. You should add and commit this file to + |# your git repository. It goes without saying that you shouldn't edit + |# this file by hand! Instead, if you wish to make changes, you should + |# change your sbt build configuration to revise the workflow description + |# to meet your needs, then regenerate this file. + | + |${renderedName} + | + |${renderedOn} + |${renderedPerm}${renderedEnv}${renderedConcurrency}${renderedJobs} + |""".stripMargin } val settingDefaults = Seq( @@ -638,7 +728,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowConcurrency := Some( Concurrency( group = s"$${{ github.workflow }} @ $${{ github.ref }}", - cancelInProgress = Some(true)) + cancelInProgress = true) ), githubWorkflowBuildMatrixFailFast := None, githubWorkflowBuildMatrixAdditions := Map(), @@ -688,7 +778,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} pathStr.replace(PlatformSep, "/") // *force* unix separators } - private val pathStrs = Def setting { + private val pathStrs = Def.setting { val base = (ThisBuild / baseDirectory).value.toPath internalTargetAggregation.value map { file => @@ -814,60 +904,79 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} WorkflowStep.SetupJava(githubWorkflowJavaVersions.value.toList) ::: githubWorkflowGeneratedCacheSteps.value.toList }, - githubWorkflowGeneratedCI := { + githubWorkflowBuildJob := { val uploadStepsOpt = - if (githubWorkflowPublishTargetBranches - .value - .isEmpty && githubWorkflowAddedJobs.value.isEmpty) + if (githubWorkflowPublishTargetBranches.value.isEmpty && + githubWorkflowAddedJobs.value.isEmpty) Nil else githubWorkflowGeneratedUploadSteps.value.toList - val publishJobOpt = Seq( - WorkflowJob( - "publish", - "Publish Artifacts", - githubWorkflowJobSetup.value.toList ::: - githubWorkflowGeneratedDownloadSteps.value.toList ::: - githubWorkflowPublishPreamble.value.toList ::: - githubWorkflowPublish.value.toList ::: - githubWorkflowPublishPostamble.value.toList, - cond = Some(publicationCond.value), - oses = githubWorkflowOSes.value.toList.take(1), - scalas = List.empty, - sbtStepPreamble = List.empty, - javas = List(githubWorkflowJavaVersions.value.head), - needs = githubWorkflowPublishNeeds.value.toList, - timeoutMinutes = githubWorkflowPublishTimeoutMinutes.value - )).filter(_ => githubWorkflowPublishTargetBranches.value.nonEmpty) - - Seq( - WorkflowJob( - "build", - "Test", - githubWorkflowJobSetup.value.toList ::: - githubWorkflowBuildPreamble.value.toList ::: - WorkflowStep.Run( - List(s"${sbt.value} githubWorkflowCheck"), - name = Some("Check that workflows are up to date")) :: - githubWorkflowBuild.value.toList ::: - githubWorkflowBuildPostamble.value.toList ::: - uploadStepsOpt, - sbtStepPreamble = githubWorkflowBuildSbtStepPreamble.value.toList, - oses = githubWorkflowOSes.value.toList, - scalas = githubWorkflowScalaVersions.value.toList, - javas = githubWorkflowJavaVersions.value.toList, - matrixFailFast = githubWorkflowBuildMatrixFailFast.value, - matrixAdds = githubWorkflowBuildMatrixAdditions.value, - matrixIncs = githubWorkflowBuildMatrixInclusions.value.toList, - matrixExcs = githubWorkflowBuildMatrixExclusions.value.toList, - runsOnExtraLabels = githubWorkflowBuildRunsOnExtraLabels.value.toList, - timeoutMinutes = githubWorkflowBuildTimeoutMinutes.value - )) ++ publishJobOpt ++ githubWorkflowAddedJobs.value + WorkflowJob.Run( + "build", + "Test", + githubWorkflowJobSetup.value.toList ::: + githubWorkflowBuildPreamble.value.toList ::: + WorkflowStep.Run( + List(s"${sbt.value} githubWorkflowCheck"), + name = Some("Check that workflows are up to date")) :: + githubWorkflowBuild.value.toList ::: + githubWorkflowBuildPostamble.value.toList ::: + uploadStepsOpt, + sbtStepPreamble = githubWorkflowBuildSbtStepPreamble.value.toList, + oses = githubWorkflowOSes.value.toList, + scalas = githubWorkflowScalaVersions.value.toList, + javas = githubWorkflowJavaVersions.value.toList, + matrixFailFast = githubWorkflowBuildMatrixFailFast.value, + matrixAdds = githubWorkflowBuildMatrixAdditions.value, + matrixIncs = githubWorkflowBuildMatrixInclusions.value.toList, + matrixExcs = githubWorkflowBuildMatrixExclusions.value.toList, + runsOnExtraLabels = githubWorkflowBuildRunsOnExtraLabels.value.toList, + timeoutMinutes = githubWorkflowBuildTimeoutMinutes.value + ) + }, + githubWorkflowPublishJob := { + WorkflowJob.Run( + "publish", + "Publish Artifacts", + githubWorkflowJobSetup.value.toList ::: + githubWorkflowGeneratedDownloadSteps.value.toList ::: + githubWorkflowPublishPreamble.value.toList ::: + githubWorkflowPublish.value.toList ::: + githubWorkflowPublishPostamble.value.toList, + cond = Some(publicationCond.value), + oses = githubWorkflowOSes.value.toList.take(1), + scalas = List.empty, + sbtStepPreamble = List.empty, + javas = List(githubWorkflowJavaVersions.value.head), + needs = githubWorkflowPublishNeeds.value.toList, + timeoutMinutes = githubWorkflowPublishTimeoutMinutes.value + ) + }, + githubWorkflowCI := toWorkflow( + name = "Continuous Integration", + branches = githubWorkflowTargetBranches.value.toList, + tags = githubWorkflowTargetTags.value.toList, + paths = githubWorkflowTargetPaths.value, + prEventTypes = githubWorkflowPREventTypes.value.toList, + permissions = githubWorkflowPermissions.value, + env = githubWorkflowEnv.value, + concurrency = githubWorkflowConcurrency.value, + jobs = githubWorkflowGeneratedCI.value.toList + ), + githubWorkflows := Map("ci" -> githubWorkflowCI.value) ++ + (if (githubWorkflowIncludeClean.value) Map("clean" -> cleanFlow) + else Map.empty), + githubWorkflowGeneratedCI := { + val publishJobOpt: Seq[WorkflowJob] = + Seq(githubWorkflowPublishJob.value).filter(_ => + githubWorkflowPublishTargetBranches.value.nonEmpty) + + Seq(githubWorkflowBuildJob.value) ++ publishJobOpt ++ githubWorkflowAddedJobs.value } ) - private val publicationCond = Def setting { + private val publicationCond = Def.setting { val publicationCondPre = githubWorkflowPublishTargetBranches .value @@ -889,31 +998,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} } } - private val generateCiContents = Def task { - compileWorkflow( - "Continuous Integration", - githubWorkflowTargetBranches.value.toList, - githubWorkflowTargetTags.value.toList, - githubWorkflowTargetPaths.value, - githubWorkflowPREventTypes.value.toList, - githubWorkflowPermissions.value, - githubWorkflowEnv.value, - githubWorkflowConcurrency.value, - githubWorkflowGeneratedCI.value.toList, - sbt.value - ) - } - - private val readCleanContents = Def task { - val src = Source.fromURL(getClass.getResource("/clean.yml")) - try { - src.mkString - } finally { - src.close() - } - } - - private val workflowsDirTask = Def task { + private val workflowsDirTask = Def.task { val githubDir = baseDirectory.value / ".github" val workflowsDir = githubDir / "workflows" @@ -928,14 +1013,6 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} workflowsDir } - private val ciYmlFile = Def task { - workflowsDirTask.value / "ci.yml" - } - - private val cleanYmlFile = Def task { - workflowsDirTask.value / "clean.yml" - } - override def projectSettings = Seq( githubWorkflowArtifactUpload := publishArtifact.value, Global / internalTargetAggregation ++= { @@ -947,25 +1024,27 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowGenerate / aggregate := false, githubWorkflowCheck / aggregate := false, githubWorkflowGenerate := { - val ciContents = generateCiContents.value - val includeClean = githubWorkflowIncludeClean.value - val cleanContents = readCleanContents.value - - val ciYml = ciYmlFile.value - val cleanYml = cleanYmlFile.value - - IO.write(ciYml, ciContents) - - if (includeClean) - IO.write(cleanYml, cleanContents) + val sbtV = sbt.value + val workflowsDir = workflowsDirTask.value + githubWorkflows + .value + .map { + case (key, value) => + (workflowsDir / s"$key.yml") -> render(value, sbtV) + } + .foreach { + case (file, contents) => + IO.write(file, contents) + } }, githubWorkflowCheck := { - val expectedCiContents = generateCiContents.value - val includeClean = githubWorkflowIncludeClean.value - val expectedCleanContents = readCleanContents.value - - val ciYml = ciYmlFile.value - val cleanYml = cleanYmlFile.value + val sbtV = sbt.value + val workflowsDir = workflowsDirTask.value + val expectedFlows: Map[File, String] = + githubWorkflows.value.map { + case (key, value) => + (workflowsDir / s"$key.yml") -> render(value, sbtV) + } val log = state.value.log @@ -983,10 +1062,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} } } - compare(ciYml, expectedCiContents) - - if (includeClean) - compare(cleanYml, expectedCleanContents) + expectedFlows.foreach(compare _ tupled) } ) @@ -1057,4 +1133,60 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} } lines.mkString("\n") } + + private val cleanFlow: Workflow = + Workflow(on = List(WorkflowTrigger.Push())) + .withName("Clean".some) + .withJobs( + WorkflowJob.Run( + id = "delete-artifacts", + name = "Delete Artifacts", + env = Map("GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), + scalas = List.empty, + javas = List.empty, + steps = WorkflowStep.Run( + name = "Delete artifacts".some, + commands = + raw"""# Customize those three lines with your repository and credentials: + |REPO=$${GITHUB_API_URL}/repos/$${{ github.repository }} + | + |# A shortcut to call GitHub API. + |ghapi() { curl --silent --location --user _:$$GITHUB_TOKEN "$$@"; } + | + |# A temporary file which receives HTTP response headers. + |TMPFILE=/tmp/tmp.$$$$ + | + |# An associative array, key: artifact name, value: number of artifacts of that name. + |declare -A ARTCOUNT + | + |# Process all artifacts on this repository, loop on returned "pages". + |URL=$$REPO/actions/artifacts + |while [[ -n "$$URL" ]]; do + | + | # Get current page, get response headers in a temporary file. + | JSON=$$(ghapi --dump-header $$TMPFILE "$$URL") + | + | # Get URL of next page. Will be empty if we are at the last page. + | URL=$$(grep '^Link:' "$$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') + | rm -f $$TMPFILE + | + | # Number of artifacts on this page: + | COUNT=$$(( $$(jq <<<$$JSON -r '.artifacts | length') )) + | + | # Loop on all artifacts on this page. + | for ((i=0; $$i < $$COUNT; i++)); do + | + | # Get name of artifact and count instances of this name. + | name=$$(jq <<<$$JSON -r ".artifacts[$$i].name?") + | ARTCOUNT[$$name]=$$(( $$(( $${ARTCOUNT[$$name]} )) + 1)) + | + | id=$$(jq <<<$$JSON -r ".artifacts[$$i].id?") + | size=$$(( $$(jq <<<$$JSON -r ".artifacts[$$i].size_in_bytes?") )) + | printf "Deleting '%s' #%d, %'d bytes\n" $$name $${ARTCOUNT[$$name]} $$size + | ghapi -X DELETE $$REPO/actions/artifacts/$$id + | done + |done""".stripMargin :: Nil + ) :: Nil + ) :: Nil + ) } diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/Secrets.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/Secrets.scala new file mode 100644 index 00000000..5a588e01 --- /dev/null +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/Secrets.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Typelevel + * + * 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 org.typelevel.sbt.gha + +sealed trait Secrets +object Secrets { + final case object Inherit extends Secrets + final case class Values(values: Map[String, String]) extends Secrets + + val empty = Values(Map.empty) +} diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/Workflow.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/Workflow.scala new file mode 100644 index 00000000..3ac0c7bd --- /dev/null +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/Workflow.scala @@ -0,0 +1,90 @@ +/* + * Copyright 2022 Typelevel + * + * 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 org.typelevel.sbt.gha + +sealed abstract class Workflow { + def name: Option[String] + def runName: Option[String] + def on: List[WorkflowTrigger] + def permissions: Option[Permissions] + def env: Map[String, String] + def concurrency: Option[Concurrency] + def jobs: List[WorkflowJob] + + // scalafmt: { maxColumn = 200 } + def withName(name: Option[String]): Workflow + def withRunName(runName: Option[String]): Workflow + def withOn(on: List[WorkflowTrigger]): Workflow + def withPermissions(permissions: Option[Permissions]): Workflow + def withEnv(env: Map[String, String]): Workflow + def withConcurrency(concurrency: Option[Concurrency]): Workflow + def withJobs(jobs: List[WorkflowJob]): Workflow + + def appendedOn(on: WorkflowTrigger): Workflow + def concatOns(suffixOn: TraversableOnce[WorkflowTrigger]): Workflow + + def updatedEnv(name: String, value: String): Workflow + def concatEnv(envs: TraversableOnce[(String, String)]): Workflow + + def appendedJob(job: WorkflowJob): Workflow + def concatJobs(suffixJobs: TraversableOnce[WorkflowJob]): Workflow + // scalafmt: { maxColumn = 96 } +} + +object Workflow { + def apply(on: List[WorkflowTrigger]): Workflow = Impl( + name = Option.empty, + runName = Option.empty, + jobs = List.empty, + on = on, + permissions = Option.empty, + env = Map.empty, + concurrency = Option.empty + ) + + private final case class Impl( + name: Option[String], + runName: Option[String], + on: List[WorkflowTrigger], + permissions: Option[Permissions], + env: Map[String, String], + concurrency: Option[Concurrency], + jobs: List[WorkflowJob] + ) extends Workflow { + + // scalafmt: { maxColumn = 200 } + override def withName(name: Option[String]): Workflow = copy(name = name) + override def withRunName(runName: Option[String]): Workflow = copy(runName = runName) + override def withOn(on: List[WorkflowTrigger]): Workflow = copy(on = on) + override def withPermissions(permissions: Option[Permissions]): Workflow = copy(permissions = permissions) + override def withEnv(env: Map[String, String]): Workflow = copy(env = env) + override def withConcurrency(concurrency: Option[Concurrency]): Workflow = copy(concurrency = concurrency) + override def withJobs(jobs: List[WorkflowJob]): Workflow = copy(jobs = jobs) + + def appendedOn(on: WorkflowTrigger): Workflow = copy(on = this.on :+ on) + def concatOns(suffixOn: TraversableOnce[WorkflowTrigger]): Workflow = copy(on = this.on ++ on) + + def updatedEnv(name: String, value: String): Workflow = copy(env = this.env.updated(name, value)) + def concatEnv(envs: TraversableOnce[(String, String)]): Workflow = copy(env = this.env ++ envs) + + override def appendedJob(job: WorkflowJob): Workflow = copy(jobs = this.jobs :+ job) + override def concatJobs(suffixJobs: TraversableOnce[WorkflowJob]): Workflow = copy(jobs = this.jobs ++ jobs) + // scalafmt: { maxColumn = 96 } + + override def productPrefix = "Workflow" + } +} diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowJob.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowJob.scala index c7253ac3..4fbe955c 100644 --- a/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowJob.scala +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowJob.scala @@ -16,53 +16,24 @@ package org.typelevel.sbt.gha -sealed abstract class WorkflowJob { +sealed abstract class WorkflowJob extends Product with Serializable { def id: String def name: String - def steps: List[WorkflowStep] - def sbtStepPreamble: List[String] - def cond: Option[String] - def permissions: Option[Permissions] - def env: Map[String, String] - def oses: List[String] - def scalas: List[String] - def javas: List[JavaSpec] def needs: List[String] - def matrixFailFast: Option[Boolean] - def matrixAdds: Map[String, List[String]] - def matrixIncs: List[MatrixInclude] - def matrixExcs: List[MatrixExclude] - def runsOnExtraLabels: List[String] - def container: Option[JobContainer] - def environment: Option[JobEnvironment] + def outputs: Map[String, String] + def permissions: Option[Permissions] def concurrency: Option[Concurrency] - def timeoutMinutes: Option[Int] + // TODO: Check for other common properites, like `cond` and `need` def withId(id: String): WorkflowJob def withName(name: String): WorkflowJob - def withSteps(steps: List[WorkflowStep]): WorkflowJob - def withSbtStepPreamble(sbtStepPreamble: List[String]): WorkflowJob - def withCond(cond: Option[String]): WorkflowJob - def withPermissions(permissions: Option[Permissions]): WorkflowJob - def withEnv(env: Map[String, String]): WorkflowJob - def withOses(oses: List[String]): WorkflowJob - def withScalas(scalas: List[String]): WorkflowJob - def withJavas(javas: List[JavaSpec]): WorkflowJob def withNeeds(needs: List[String]): WorkflowJob - def withMatrixFailFast(matrixFailFast: Option[Boolean]): WorkflowJob - def withMatrixAdds(matrixAdds: Map[String, List[String]]): WorkflowJob - def withMatrixIncs(matrixIncs: List[MatrixInclude]): WorkflowJob - def withMatrixExcs(matrixExcs: List[MatrixExclude]): WorkflowJob - def withRunsOnExtraLabels(runsOnExtraLabels: List[String]): WorkflowJob - def withContainer(container: Option[JobContainer]): WorkflowJob - def withEnvironment(environment: Option[JobEnvironment]): WorkflowJob + def withOutputs(outputs: Map[String, String]): WorkflowJob + def withPermissions(permissions: Option[Permissions]): WorkflowJob def withConcurrency(concurrency: Option[Concurrency]): WorkflowJob - def withTimeoutMinutes(timeoutMinutes: Option[Int]): WorkflowJob - def updatedEnv(name: String, value: String): WorkflowJob - def concatEnv(envs: TraversableOnce[(String, String)]): WorkflowJob - def appendedStep(step: WorkflowStep): WorkflowJob - def concatSteps(suffixSteps: TraversableOnce[WorkflowStep]): WorkflowJob + def updatedOutputs(name: String, value: String): WorkflowJob + def concatOutputs(outputs: TraversableOnce[(String, String)]): WorkflowJob } object WorkflowJob { @@ -74,6 +45,7 @@ object WorkflowJob { cond: Option[String] = None, permissions: Option[Permissions] = None, env: Map[String, String] = Map(), + outputs: Map[String, String] = Map.empty, oses: List[String] = List("ubuntu-22.04"), scalas: List[String] = List("2.13"), javas: List[JavaSpec] = List(JavaSpec.temurin("11")), @@ -86,81 +58,271 @@ object WorkflowJob { container: Option[JobContainer] = None, environment: Option[JobEnvironment] = None, concurrency: Option[Concurrency] = None, - timeoutMinutes: Option[Int] = None): WorkflowJob = - Impl( - id, - name, - steps, - sbtStepPreamble, - cond, - permissions, - env, - oses, - scalas, - javas, - needs, - matrixFailFast, - matrixAdds, - matrixIncs, - matrixExcs, - runsOnExtraLabels, - container, - environment, - concurrency, - timeoutMinutes + timeoutMinutes: Option[Int] = None + ): Run = + Run( + id = id, + name = name, + steps = steps, + sbtStepPreamble = sbtStepPreamble, + cond = cond, + permissions = permissions, + env = env, + outputs = outputs, + oses = oses, + scalas = scalas, + javas = javas, + needs = needs, + matrixFailFast = matrixFailFast, + matrixAdds = matrixAdds, + matrixIncs = matrixIncs, + matrixExcs = matrixExcs, + runsOnExtraLabels = runsOnExtraLabels, + container = container, + environment = environment, + concurrency = concurrency, + timeoutMinutes = timeoutMinutes ) + sealed abstract class Run extends WorkflowJob { + def id: String + def name: String + def steps: List[WorkflowStep] + def sbtStepPreamble: List[String] + def cond: Option[String] + def permissions: Option[Permissions] + def env: Map[String, String] + def outputs: Map[String, String] + def oses: List[String] + def scalas: List[String] + def javas: List[JavaSpec] + def needs: List[String] + def matrixFailFast: Option[Boolean] + def matrixAdds: Map[String, List[String]] + def matrixIncs: List[MatrixInclude] + def matrixExcs: List[MatrixExclude] + def runsOnExtraLabels: List[String] + def container: Option[JobContainer] + def environment: Option[JobEnvironment] + def concurrency: Option[Concurrency] + def timeoutMinutes: Option[Int] - private final case class Impl( - id: String, - name: String, - steps: List[WorkflowStep], - sbtStepPreamble: List[String], - cond: Option[String], - permissions: Option[Permissions], - env: Map[String, String], - oses: List[String], - scalas: List[String], - javas: List[JavaSpec], - needs: List[String], - matrixFailFast: Option[Boolean], - matrixAdds: Map[String, List[String]], - matrixIncs: List[MatrixInclude], - matrixExcs: List[MatrixExclude], - runsOnExtraLabels: List[String], - container: Option[JobContainer], - environment: Option[JobEnvironment], - concurrency: Option[Concurrency], - timeoutMinutes: Option[Int]) - extends WorkflowJob { - - // scalafmt: { maxColumn = 200 } - override def withId(id: String): WorkflowJob = copy(id = id) - override def withName(name: String): WorkflowJob = copy(name = name) - override def withSteps(steps: List[WorkflowStep]): WorkflowJob = copy(steps = steps) - override def withSbtStepPreamble(sbtStepPreamble: List[String]): WorkflowJob = copy(sbtStepPreamble = sbtStepPreamble) - override def withCond(cond: Option[String]): WorkflowJob = copy(cond = cond) - override def withPermissions(permissions: Option[Permissions]): WorkflowJob = copy(permissions = permissions) - override def withEnv(env: Map[String, String]): WorkflowJob = copy(env = env) - override def withOses(oses: List[String]): WorkflowJob = copy(oses = oses) - override def withScalas(scalas: List[String]): WorkflowJob = copy(scalas = scalas) - override def withJavas(javas: List[JavaSpec]): WorkflowJob = copy(javas = javas) - override def withNeeds(needs: List[String]): WorkflowJob = copy(needs = needs) - override def withMatrixFailFast(matrixFailFast: Option[Boolean]): WorkflowJob = copy(matrixFailFast = matrixFailFast) - override def withMatrixAdds(matrixAdds: Map[String, List[String]]): WorkflowJob = copy(matrixAdds = matrixAdds) - override def withMatrixIncs(matrixIncs: List[MatrixInclude]): WorkflowJob = copy(matrixIncs = matrixIncs) - override def withMatrixExcs(matrixExcs: List[MatrixExclude]): WorkflowJob = copy(matrixExcs = matrixExcs) - override def withRunsOnExtraLabels(runsOnExtraLabels: List[String]): WorkflowJob = copy(runsOnExtraLabels = runsOnExtraLabels) - override def withContainer(container: Option[JobContainer]): WorkflowJob = copy(container = container) - override def withEnvironment(environment: Option[JobEnvironment]): WorkflowJob = copy(environment = environment) - override def withConcurrency(concurrency: Option[Concurrency]): WorkflowJob = copy(concurrency = concurrency) - override def withTimeoutMinutes(timeoutMinutes: Option[Int]): WorkflowJob = copy(timeoutMinutes = timeoutMinutes) - - def updatedEnv(name: String, value: String): WorkflowJob = copy(env = env.updated(name, value)) - def concatEnv(envs: TraversableOnce[(String, String)]): WorkflowJob = copy(env = this.env ++ envs) - def appendedStep(step: WorkflowStep): WorkflowJob = copy(steps = this.steps :+ step) - def concatSteps(suffixSteps: TraversableOnce[WorkflowStep]): WorkflowJob = copy(steps = this.steps ++ suffixSteps) - // scalafmt: { maxColumn = 96 } - - override def productPrefix = "WorkflowJob" + def withId(id: String): Run + def withName(name: String): Run + def withSteps(steps: List[WorkflowStep]): Run + def withSbtStepPreamble(sbtStepPreamble: List[String]): Run + def withCond(cond: Option[String]): Run + def withPermissions(permissions: Option[Permissions]): Run + def withEnv(env: Map[String, String]): Run + def withOutputs(outputs: Map[String, String]): Run + def withOses(oses: List[String]): Run + def withScalas(scalas: List[String]): Run + def withJavas(javas: List[JavaSpec]): Run + def withNeeds(needs: List[String]): Run + def withMatrixFailFast(matrixFailFast: Option[Boolean]): Run + def withMatrixAdds(matrixAdds: Map[String, List[String]]): Run + def withMatrixIncs(matrixIncs: List[MatrixInclude]): Run + def withMatrixExcs(matrixExcs: List[MatrixExclude]): Run + def withRunsOnExtraLabels(runsOnExtraLabels: List[String]): Run + def withContainer(container: Option[JobContainer]): Run + def withEnvironment(environment: Option[JobEnvironment]): Run + def withConcurrency(concurrency: Option[Concurrency]): Run + def withTimeoutMinutes(timeoutMinutes: Option[Int]): Run + + def updatedEnv(name: String, value: String): Run + def concatEnv(envs: TraversableOnce[(String, String)]): Run + def updatedOutputs(name: String, value: String): Run + def concatOutputs(outputs: TraversableOnce[(String, String)]): Run + def appendedStep(step: WorkflowStep): Run + def concatSteps(suffixSteps: TraversableOnce[WorkflowStep]): Run + } + object Run { + def apply( + id: String, + name: String, + steps: List[WorkflowStep], + sbtStepPreamble: List[String] = List(s"++ $${{ matrix.scala }}"), + cond: Option[String] = None, + permissions: Option[Permissions] = None, + env: Map[String, String] = Map(), + outputs: Map[String, String] = Map.empty, + oses: List[String] = List("ubuntu-22.04"), + scalas: List[String] = List("2.13"), + javas: List[JavaSpec] = List(JavaSpec.temurin("11")), + needs: List[String] = List(), + matrixFailFast: Option[Boolean] = None, + matrixAdds: Map[String, List[String]] = Map(), + matrixIncs: List[MatrixInclude] = List(), + matrixExcs: List[MatrixExclude] = List(), + runsOnExtraLabels: List[String] = List(), + container: Option[JobContainer] = None, + environment: Option[JobEnvironment] = None, + concurrency: Option[Concurrency] = None, + timeoutMinutes: Option[Int] = None + ): Run = + Impl( + id = id, + name = name, + steps = steps, + sbtStepPreamble = sbtStepPreamble, + cond = cond, + permissions = permissions, + env = env, + outputs = outputs, + oses = oses, + scalas = scalas, + javas = javas, + needs = needs, + matrixFailFast = matrixFailFast, + matrixAdds = matrixAdds, + matrixIncs = matrixIncs, + matrixExcs = matrixExcs, + runsOnExtraLabels = runsOnExtraLabels, + container = container, + environment = environment, + concurrency = concurrency, + timeoutMinutes = timeoutMinutes + ) + private final case class Impl( + id: String, + name: String, + steps: List[WorkflowStep], + sbtStepPreamble: List[String], + cond: Option[String], + permissions: Option[Permissions], + env: Map[String, String], + outputs: Map[String, String], + oses: List[String], + scalas: List[String], + javas: List[JavaSpec], + needs: List[String], + matrixFailFast: Option[Boolean], + matrixAdds: Map[String, List[String]], + matrixIncs: List[MatrixInclude], + matrixExcs: List[MatrixExclude], + runsOnExtraLabels: List[String], + container: Option[JobContainer], + environment: Option[JobEnvironment], + concurrency: Option[Concurrency], + timeoutMinutes: Option[Int] + ) extends Run { + + // scalafmt: { maxColumn = 200 } + override def withId(id: String): Run = copy(id = id) + override def withName(name: String): Run = copy(name = name) + override def withSteps(steps: List[WorkflowStep]): Run = copy(steps = steps) + override def withSbtStepPreamble(sbtStepPreamble: List[String]): Run = copy(sbtStepPreamble = sbtStepPreamble) + override def withCond(cond: Option[String]): Run = copy(cond = cond) + override def withPermissions(permissions: Option[Permissions]): Run = copy(permissions = permissions) + override def withEnv(env: Map[String, String]): Run = copy(env = env) + override def withOutputs(outputs: Map[String, String]): Run = copy(outputs = outputs) + override def withOses(oses: List[String]): Run = copy(oses = oses) + override def withScalas(scalas: List[String]): Run = copy(scalas = scalas) + override def withJavas(javas: List[JavaSpec]): Run = copy(javas = javas) + override def withNeeds(needs: List[String]): Run = copy(needs = needs) + override def withMatrixFailFast(matrixFailFast: Option[Boolean]): Run = copy(matrixFailFast = matrixFailFast) + override def withMatrixAdds(matrixAdds: Map[String, List[String]]): Run = copy(matrixAdds = matrixAdds) + override def withMatrixIncs(matrixIncs: List[MatrixInclude]): Run = copy(matrixIncs = matrixIncs) + override def withMatrixExcs(matrixExcs: List[MatrixExclude]): Run = copy(matrixExcs = matrixExcs) + override def withRunsOnExtraLabels(runsOnExtraLabels: List[String]): Run = copy(runsOnExtraLabels = runsOnExtraLabels) + override def withContainer(container: Option[JobContainer]): Run = copy(container = container) + override def withEnvironment(environment: Option[JobEnvironment]): Run = copy(environment = environment) + override def withConcurrency(concurrency: Option[Concurrency]): Run = copy(concurrency = concurrency) + override def withTimeoutMinutes(timeoutMinutes: Option[Int]): Run = copy(timeoutMinutes = timeoutMinutes) + + override def updatedEnv(name: String, value: String): Run = copy(env = env.updated(name, value)) + override def concatEnv(envs: TraversableOnce[(String, String)]): Run = copy(env = this.env ++ envs) + override def updatedOutputs(name: String, value: String): Run = copy(outputs = outputs.updated(name, value)) + override def concatOutputs(outputs: TraversableOnce[(String, String)]): Run = copy(outputs = this.outputs ++ outputs) + override def appendedStep(step: WorkflowStep): Run = copy(steps = this.steps :+ step) + override def concatSteps(suffixSteps: TraversableOnce[WorkflowStep]): Run = copy(steps = this.steps ++ suffixSteps) + // scalafmt: { maxColumn = 96 } + + override def productPrefix = "WorkflowJob" + } + } + + sealed abstract class Use extends WorkflowJob { + def id: String + def name: String + def uses: String + def needs: List[String] + def secrets: Option[Secrets] + def params: Map[String, String] + def outputs: Map[String, String] + def permissions: Option[Permissions] + def concurrency: Option[Concurrency] + + def withId(id: String): Use + def withName(name: String): Use + def withNeeds(needs: List[String]): Use + def withUses(uses: String): Use + def withSecrets(secrets: Option[Secrets]): Use + def withParams(params: Map[String, String]): Use + def withOutputs(outputs: Map[String, String]): Use + def withPermissions(permissions: Option[Permissions]): Use + def withConcurrency(concurrency: Option[Concurrency]): Use + + def updatedParams(name: String, value: String): Use + def concatParams(params: TraversableOnce[(String, String)]): Use + def updatedOutputs(name: String, value: String): Use + def concatOutputs(outputs: TraversableOnce[(String, String)]): Use + } + object Use { + def apply( + id: String, + name: String, + uses: String, + needs: List[String] = List.empty, + secrets: Option[Secrets] = None, + params: Map[String, String] = Map.empty, + outputs: Map[String, String] = Map.empty, + permissions: Option[Permissions] = None, + concurrency: Option[Concurrency] = None + ): Use = new Impl( + id = id, + name = name, + uses = uses, + needs = needs, + secrets = secrets, + params = params, + outputs = outputs, + permissions = permissions, + concurrency = concurrency + ) + private final case class Impl( + id: String, + name: String, + uses: String, + needs: List[String], + secrets: Option[Secrets], + params: Map[String, String], + outputs: Map[String, String], + permissions: Option[Permissions], + concurrency: Option[Concurrency] + ) extends Use { + override def productPrefix = "Use" + + // scalafmt: { maxColumn = 200 } + override def withId(id: String): Use = copy(id = id) + override def withName(name: String): Use = copy(name = name) + override def withNeeds(needs: List[String]): Use = copy(needs = needs) + override def withUses(uses: String): Use = copy(uses = uses) + override def withSecrets(secrets: Option[Secrets]): Use = copy(secrets = secrets) + override def withParams(params: Map[String, String]): Use = copy(params = params) + override def withOutputs(outputs: Map[String, String]): Use = copy(outputs = outputs) + override def withPermissions(permissions: Option[Permissions]): Use = copy(permissions = permissions) + override def withConcurrency(concurrency: Option[Concurrency]): Use = copy(concurrency = concurrency) + // scalafmt: { maxColumn = 96 } + + override def updatedParams(name: String, value: String) = + copy(params = params.updated(name, value)) + override def concatParams(params: TraversableOnce[(String, String)]) = + copy(params = this.params ++ params) + + override def updatedOutputs(name: String, value: String): Use = + copy(outputs = outputs.updated(name, value)) + override def concatOutputs(outputs: TraversableOnce[(String, String)]): Use = + copy(outputs = this.outputs ++ outputs) + } } } diff --git a/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowTrigger.scala b/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowTrigger.scala new file mode 100644 index 00000000..3303b5a4 --- /dev/null +++ b/github-actions/src/main/scala/org/typelevel/sbt/gha/WorkflowTrigger.scala @@ -0,0 +1,330 @@ +/* + * Copyright 2022 Typelevel + * + * 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 org.typelevel.sbt.gha + +sealed trait WorkflowTrigger +object WorkflowTrigger { + sealed trait BranchesFilter extends Product with Serializable + object BranchesFilter { + final case class Branches(branches: List[String]) extends BranchesFilter + final case class BranchesIgnore(branches: List[String]) extends BranchesFilter + } + + sealed trait TagsFilter extends Product with Serializable + object TagsFilter { + final case class Tags(tags: List[String]) extends TagsFilter + final case class TagsIgnore(tags: List[String]) extends TagsFilter + } + + sealed trait PullRequest extends WorkflowTrigger { + def paths: Paths + def branchesFilter: Option[BranchesFilter] + def types: List[PREventType] + + def withBranchesFilter(filter: Option[BranchesFilter]): PullRequest + def withPaths(paths: Paths): PullRequest + def withTypes(types: List[PREventType]): PullRequest + } + object PullRequest { + def apply( + branchesFilter: Option[BranchesFilter] = None, + paths: Paths = Paths.None, + types: List[PREventType] = List.empty + ): PullRequest = Impl( + branchesFilter = branchesFilter, + paths = paths, + types = types + ) + + private final case class Impl( + branchesFilter: Option[BranchesFilter], + paths: Paths, + types: List[PREventType] + ) extends PullRequest { + override def productPrefix = "PullRequest" + + // scalafmt: { maxColumn = 200 } + def withPaths(paths: Paths): PullRequest = copy(paths = paths) + def withBranchesFilter(filter: Option[BranchesFilter]): PullRequest = copy(branchesFilter = filter) + def withTypes(types: List[PREventType]): PullRequest = copy(types = types) + // scalafmt: { maxColumn = 96 } + } + } + + sealed trait Push extends WorkflowTrigger { + def branchesFilter: Option[BranchesFilter] + def tagsFilter: Option[TagsFilter] + def paths: Paths + + def withBranchesFilter(filter: Option[BranchesFilter]): Push + def withTagsFilter(filter: Option[TagsFilter]): Push + def withPaths(paths: Paths): Push + } + object Push { + def apply( + branchesFilter: Option[BranchesFilter] = None, + tagsFilter: Option[TagsFilter] = None, + paths: Paths = Paths.None + ): Push = Impl( + branchesFilter = branchesFilter, + tagsFilter = tagsFilter, + paths = paths + ) + + private final case class Impl( + branchesFilter: Option[BranchesFilter], + tagsFilter: Option[TagsFilter], + paths: Paths + ) extends Push { + override def productPrefix = "Push" + + // scalafmt: { maxColumn = 200 } + def withBranchesFilter(filter: Option[BranchesFilter]): Push = copy(branchesFilter = filter) + def withTagsFilter(filter: Option[TagsFilter]): Push = copy(tagsFilter = filter) + def withPaths(paths: Paths): Push = copy(paths = paths) + // scalafmt: { maxColumn = 96 } + } + } + + sealed trait WorkflowCall extends WorkflowTrigger { + def inputs: Map[String, WorkflowCallInput] + + def withInputs(value: Map[String, WorkflowCallInput]): WorkflowCall + def updatedInputs(id: String, value: WorkflowCallInput): WorkflowCall + } + object WorkflowCall { + def apply(inputs: (String, WorkflowCallInput)*): WorkflowTrigger = + Impl(inputs = inputs.toMap) + + private final case class Impl( + inputs: Map[String, WorkflowCallInput] + ) extends WorkflowCall { + override def productPrefix = "WorkflowCall" + + override def withInputs(value: Map[String, WorkflowCallInput]): WorkflowCall = + copy(inputs = value) + override def updatedInputs(id: String, value: WorkflowCallInput): WorkflowCall = + copy(inputs = this.inputs.updated(id, value)) + } + } + + sealed trait WorkflowDispatch extends WorkflowTrigger { + def inputs: Map[String, WorkflowDispatchInput] + + def withInputs(value: Map[String, WorkflowDispatchInput]): WorkflowDispatch + def updatedInputs(id: String, value: WorkflowDispatchInput): WorkflowDispatch + } + object WorkflowDispatch { + def apply(inputs: (String, WorkflowDispatchInput)*): WorkflowTrigger = + Impl(inputs = inputs.toMap) + + private final case class Impl( + inputs: Map[String, WorkflowDispatchInput] + ) extends WorkflowDispatch { + override def productPrefix = "WorkflowDispatch" + + override def withInputs(value: Map[String, WorkflowDispatchInput]): WorkflowDispatch = + copy(inputs = value) + override def updatedInputs(id: String, value: WorkflowDispatchInput): WorkflowDispatch = + copy(inputs = this.inputs.updated(id, value)) + } + } + + sealed trait WorkflowDispatchInput { + def `type`: WorkflowDispatchInputType + def description: Option[String] + def required: Boolean + def default: Option[String] + + def withType(value: WorkflowDispatchInputType): WorkflowDispatchInput + def withDescription(value: Option[String]): WorkflowDispatchInput + def withRequired(value: Boolean): WorkflowDispatchInput + def withDefault(value: Option[String]): WorkflowDispatchInput + } + object WorkflowDispatchInput { + def apply( + required: Boolean, + `type`: WorkflowDispatchInputType + ): WorkflowDispatchInput = Impl( + `type` = `type`, + required = required, + default = None, + description = None + ) + private final case class Impl( + `type`: WorkflowDispatchInputType, + description: Option[String], + required: Boolean, + default: Option[String] + ) extends WorkflowDispatchInput { + override def productPrefix = "WorkflowDispatchInput" + + override def withType(value: WorkflowDispatchInputType): WorkflowDispatchInput = + copy(`type` = value) + override def withDescription(value: Option[String]): WorkflowDispatchInput = + copy(description = value) + override def withRequired(value: Boolean): WorkflowDispatchInput = + copy(required = value) + override def withDefault(value: Option[String]): WorkflowDispatchInput = + copy(default = value) + } + } + sealed trait WorkflowDispatchInputType extends Product with Serializable + object WorkflowDispatchInputType { + final case object Boolean extends WorkflowDispatchInputType + final case object Number extends WorkflowDispatchInputType + final case object Environment extends WorkflowDispatchInputType + final case object String extends WorkflowDispatchInputType + final case class Choice(options: List[String]) extends WorkflowDispatchInputType + } + + sealed trait WorkflowCallInput { + def `type`: WorkflowCallInputType + def description: Option[String] + def required: Boolean + def default: Option[String] + + def withType(value: WorkflowCallInputType): WorkflowCallInput + def withDescription(value: Option[String]): WorkflowCallInput + def withRequired(value: Boolean): WorkflowCallInput + def withDefault(value: Option[String]): WorkflowCallInput + } + object WorkflowCallInput { + def apply( + required: Boolean, + `type`: WorkflowCallInputType + ): WorkflowCallInput = Impl( + `type` = `type`, + required = required, + default = None, + description = None + ) + private final case class Impl( + `type`: WorkflowCallInputType, + description: Option[String], + required: Boolean, + default: Option[String] + ) extends WorkflowCallInput { + override def productPrefix = "WorkflowCallInput" + + override def withType(value: WorkflowCallInputType) = + copy(`type` = value) + override def withDescription(value: Option[String]) = + copy(description = value) + override def withRequired(value: Boolean) = + copy(required = value) + override def withDefault(value: Option[String]) = + copy(default = value) + } + } + sealed trait WorkflowCallInputType extends Product with Serializable + object WorkflowCallInputType { + final case object Boolean extends WorkflowCallInputType + final case object Number extends WorkflowCallInputType + final case object String extends WorkflowCallInputType + } + + // TODO: workflow_run + sealed trait WorkflowRun { + def workflows: List[String] + def types: List[WorkflowRunType] + def filter: WorkflowRun + } + + sealed trait WorkflowRunType extends Product with Serializable + object WorkflowRunTypes { + case object Commpleted extends WorkflowRunType + case object Requested extends WorkflowRunType + case object InProgress extends WorkflowRunType + } + + /** + * A workflow trigger, inserted directly into the yaml, with 1 level of indention. This is an + * escape hatch for people wanting triggers other than the supported ones. + */ + sealed trait Raw extends WorkflowTrigger { + def toYaml: String + } + object Raw { + def raw(yaml: String): Raw = Impl(yaml) + + private final case class Impl(toYaml: String) extends Raw { + override def productPrefix = "Raw" + } + } + + /* + * Other Triggers not implemented here: + * + * branch_protection_rule + * check_run + * check_suite + * create + * delete + * deployment + * deployment_status + * discussion + * discussion_comment + * fork + * gollum + * issue_comment + * issues + * label + * merge_group + * milestone + * page_build + * public + * pull_request_comment (use issue_comment) + * pull_request_review + * pull_request_review_comment + * pull_request_target + * registry_package + * release + * repository_dispatch + * schedule + * status + * watch + * workflow_disbranch_protection_rule + * check_run + * check_suite + * create + * delete + * deployment + * deployment_status + * discussion + * discussion_comment + * fork + * gollum + * issue_comment + * issues + * label + * merge_group + * milestone + * page_build + * public + * pull_request_comment (use issue_comment) + * pull_request_review + * pull_request_review_comment + * pull_request_target + * registry_package + * release + * repository_dispatch + * schedule + * status + * watch + */ +} diff --git a/mergify/src/main/scala/org/typelevel/sbt/mergify/MergifyPlugin.scala b/mergify/src/main/scala/org/typelevel/sbt/mergify/MergifyPlugin.scala index 360c5120..9d444ba4 100644 --- a/mergify/src/main/scala/org/typelevel/sbt/mergify/MergifyPlugin.scala +++ b/mergify/src/main/scala/org/typelevel/sbt/mergify/MergifyPlugin.scala @@ -135,7 +135,7 @@ object MergifyPlugin extends AutoPlugin { private lazy val jobSuccessConditions = Def.setting { githubWorkflowGeneratedCI.value.flatMap { - case job if mergifyRequiredJobs.value.contains(job.id) => + case job: WorkflowJob.Run if mergifyRequiredJobs.value.contains(job.id) => GenerativePlugin .expandMatrix( job.oses, @@ -148,6 +148,8 @@ object MergifyPlugin extends AutoPlugin { .map { cell => MergifyCondition.Custom(s"status-success=${job.name} (${cell.mkString(", ")})") } + case job: WorkflowJob.Use if mergifyRequiredJobs.value.contains(job.id) => + MergifyCondition.Custom(s"status-success=${job.name}") :: Nil case _ => Nil } } diff --git a/site/src/main/scala/org/typelevel/sbt/TypelevelSitePlugin.scala b/site/src/main/scala/org/typelevel/sbt/TypelevelSitePlugin.scala index 24f6ba0c..f7b178f6 100644 --- a/site/src/main/scala/org/typelevel/sbt/TypelevelSitePlugin.scala +++ b/site/src/main/scala/org/typelevel/sbt/TypelevelSitePlugin.scala @@ -218,7 +218,7 @@ object TypelevelSitePlugin extends AutoPlugin { WorkflowStep.SetupJava(List(tlSiteJavaVersion.value)) else Nil - WorkflowJob( + WorkflowJob.Run( "site", "Generate Site", scalas = List.empty,