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,