diff --git a/src/main/scala/sbtghactions/GenerativeKeys.scala b/src/main/scala/sbtghactions/GenerativeKeys.scala index 93b338c..de69191 100644 --- a/src/main/scala/sbtghactions/GenerativeKeys.scala +++ b/src/main/scala/sbtghactions/GenerativeKeys.scala @@ -62,6 +62,7 @@ trait GenerativeKeys { lazy val githubWorkflowJobSetup = settingKey[Seq[WorkflowStep]]("The automatically-generated checkout, setup, and cache steps which are common to all jobs which touch the build (default: autogenerated)") lazy val githubWorkflowEnv = settingKey[Map[String, String]](s"A map of static environment variable assignments global to the workflow (default: { GITHUB_TOKEN: $${{ secrets.GITHUB_TOKEN }} })") + lazy val githubWorkflowPermissions = settingKey[Option[Permissions]](s"Permissions to use for the global workflow (default: None)") lazy val githubWorkflowAddedJobs = settingKey[Seq[WorkflowJob]]("A list of additional jobs to add to the CI workflow (default: [])") } diff --git a/src/main/scala/sbtghactions/GenerativePlugin.scala b/src/main/scala/sbtghactions/GenerativePlugin.scala index f635db0..c0f4edb 100644 --- a/src/main/scala/sbtghactions/GenerativePlugin.scala +++ b/src/main/scala/sbtghactions/GenerativePlugin.scala @@ -186,6 +186,48 @@ s"""$prefix: ${indent(rendered.mkString("\n"), 1)}""" } + def compilePermissionScope(permissionScope: PermissionScope): String = permissionScope match { + case PermissionScope.Actions => "actions" + case PermissionScope.Checks => "checks" + case PermissionScope.Contents => "contents" + case PermissionScope.Deployments => "deployments" + case PermissionScope.IdToken => "id-token" + case PermissionScope.Issues => "issues" + case PermissionScope.Discussions => "discussions" + case PermissionScope.Packages => "packages" + case PermissionScope.Pages => "pages" + case PermissionScope.PullRequests => "pull-requests" + case PermissionScope.RepositoryProjects => "repository-projects" + case PermissionScope.SecurityEvents => "security-events" + case PermissionScope.Statuses => "statuses" + } + + def compilePermissionsValue(permissionValue: PermissionValue): String = permissionValue match { + case PermissionValue.Read => "read" + case PermissionValue.Write => "write" + case PermissionValue.None => "none" + } + + def compilePermissions(permissions: Option[Permissions]): String = { + permissions match { + case Some(perms) => + val rendered = perms match { + case Permissions.ReadAll => " read-all" + case Permissions.WriteAll => " write-all" + case Permissions.None => " {}" + case Permissions.Specify(permMap) => + val map = permMap.map{ + case (key, value) => + s"${compilePermissionScope(key)}: ${compilePermissionsValue(value)}" + } + "\n" + indent(map.mkString("\n"), 1) + } + s"permissions:$rendered" + + case None => "" + } + } + def compileStep(step: WorkflowStep, sbt: String, declareShell: Boolean = false): String = { import WorkflowStep._ @@ -331,6 +373,12 @@ ${indent(rendered.mkString("\n"), 1)}""" else "\n" + renderedEnvPre + val renderedPermPre = compilePermissions(job.permissions) + val renderedPerm = if (renderedPermPre.isEmpty) + "" + else + "\n" + renderedPermPre + 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") @@ -407,7 +455,7 @@ strategy:${renderedFailFast} os:${compileList(job.oses, 3)} scala:${compileList(job.scalas, 3)} java:${compileList(job.javas.map(_.render), 3)}${renderedMatrices} -runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedEnv} +runs-on: ${runsOn}${renderedEnvironment}${renderedContainer}${renderedPerm}${renderedEnv} steps: ${indent(job.steps.map(compileStep(_, sbt, declareShell = declareShell)).mkString("\n\n"), 1)}""" @@ -420,16 +468,22 @@ ${indent(job.steps.map(compileStep(_, sbt, declareShell = declareShell)).mkStrin tags: List[String], paths: Paths, prEventTypes: List[PREventType], + permissions: Option[Permissions], env: Map[String, String], jobs: List[WorkflowJob], sbt: String) : String = { + val renderedPermissionsPre = compilePermissions(permissions) val renderedEnvPre = compileEnv(env) val renderedEnv = if (renderedEnvPre.isEmpty) "" else renderedEnvPre + "\n\n" + val renderedPerm = if (renderedPermissionsPre.isEmpty) + "" + else + renderedPermissionsPre + "\n\n" val renderedTypesPre = prEventTypes.map(compilePREventType).mkString("[", ", ", "]") val renderedTypes = if (prEventTypes.sortBy(_.toString) == PREventType.Defaults) @@ -467,7 +521,7 @@ on: push: branches: [${branches.map(wrap).mkString(", ")}]$renderedTags$renderedPaths -${renderedEnv}jobs: +${renderedPerm}${renderedEnv}jobs: ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} """ } @@ -504,6 +558,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowTargetPaths := Paths.None, githubWorkflowEnv := Map("GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), + githubWorkflowPermissions := None, githubWorkflowAddedJobs := Seq()) private lazy val internalTargetAggregation = settingKey[Seq[File]]("Aggregates target directories from all subprojects") @@ -697,6 +752,7 @@ ${indent(jobs.map(compileJob(_, sbt)).mkString("\n\n"), 1)} githubWorkflowTargetTags.value.toList, githubWorkflowTargetPaths.value, githubWorkflowPREventTypes.value.toList, + githubWorkflowPermissions.value, githubWorkflowEnv.value, githubWorkflowGeneratedCI.value.toList, sbt) diff --git a/src/main/scala/sbtghactions/PermissionScope.scala b/src/main/scala/sbtghactions/PermissionScope.scala new file mode 100644 index 0000000..214628f --- /dev/null +++ b/src/main/scala/sbtghactions/PermissionScope.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2020-2021 Daniel Spiewak + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sbtghactions + +sealed trait Permissions extends Product with Serializable + +/** + * @see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs#overview + */ +object Permissions { + case object ReadAll extends Permissions + case object WriteAll extends Permissions + case object None extends Permissions + final case class Specify(values: Map[PermissionScope, PermissionValue]) extends Permissions +} + +sealed trait PermissionScope extends Product with Serializable + +object PermissionScope { + case object Actions extends PermissionScope + case object Checks extends PermissionScope + case object Contents extends PermissionScope + case object Deployments extends PermissionScope + case object IdToken extends PermissionScope + case object Issues extends PermissionScope + case object Discussions extends PermissionScope + case object Packages extends PermissionScope + case object Pages extends PermissionScope + case object PullRequests extends PermissionScope + case object RepositoryProjects extends PermissionScope + case object SecurityEvents extends PermissionScope + case object Statuses extends PermissionScope +} + +sealed trait PermissionValue extends Product with Serializable + +object PermissionValue { + case object Read extends PermissionValue + case object Write extends PermissionValue + case object None extends PermissionValue +} diff --git a/src/main/scala/sbtghactions/WorkflowJob.scala b/src/main/scala/sbtghactions/WorkflowJob.scala index c1b8c20..3d5f1ad 100644 --- a/src/main/scala/sbtghactions/WorkflowJob.scala +++ b/src/main/scala/sbtghactions/WorkflowJob.scala @@ -21,6 +21,7 @@ final case class WorkflowJob( name: String, steps: List[WorkflowStep], cond: Option[String] = None, + permissions: Option[Permissions] = None, env: Map[String, String] = Map(), oses: List[String] = List("ubuntu-latest"), scalas: List[String] = List("2.13.6"), diff --git a/src/test/scala/sbtghactions/GenerativePluginSpec.scala b/src/test/scala/sbtghactions/GenerativePluginSpec.scala index e544d96..a6adbb0 100644 --- a/src/test/scala/sbtghactions/GenerativePluginSpec.scala +++ b/src/test/scala/sbtghactions/GenerativePluginSpec.scala @@ -46,7 +46,7 @@ class GenerativePluginSpec extends Specification { |${" " * 2} |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.None, PREventType.Defaults, Map(), Nil, "sbt") mustEqual expected + compileWorkflow("test", List("main"), Nil, Paths.None, PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected } "produce the appropriate skeleton around a zero-job workflow with non-empty tags" in { @@ -64,7 +64,7 @@ class GenerativePluginSpec extends Specification { |${" " * 2} |""".stripMargin - compileWorkflow("test", List("main"), List("howdy"), Paths.None, PREventType.Defaults, Map(), Nil, "sbt") mustEqual expected + compileWorkflow("test", List("main"), List("howdy"), Paths.None, PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected } "respect non-default pr types" in { @@ -82,7 +82,7 @@ class GenerativePluginSpec extends Specification { |${" " * 2} |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.None, List(PREventType.ReadyForReview, PREventType.ReviewRequested, PREventType.Opened), Map(), Nil, "sbt") mustEqual expected + compileWorkflow("test", List("main"), Nil, Paths.None, List(PREventType.ReadyForReview, PREventType.ReviewRequested, PREventType.Opened), None, Map(), Nil, "sbt") mustEqual expected } "compile a one-job workflow targeting multiple branch patterns with a environment variables" in { @@ -95,6 +95,9 @@ class GenerativePluginSpec extends Specification { | push: | branches: [main, backport/v*] | + |permissions: + | id-token: write + | |env: | GITHUB_TOKEN: $${{ secrets.GITHUB_TOKEN }} | @@ -117,6 +120,9 @@ class GenerativePluginSpec extends Specification { Nil, Paths.None, PREventType.Defaults, + Some(Permissions.Specify(Map( + PermissionScope.IdToken -> PermissionValue.Write + ))), Map( "GITHUB_TOKEN" -> s"$${{ secrets.GITHUB_TOKEN }}"), List( @@ -167,6 +173,7 @@ class GenerativePluginSpec extends Specification { Nil, Paths.None, PREventType.Defaults, + None, Map(), List( WorkflowJob( @@ -211,6 +218,7 @@ class GenerativePluginSpec extends Specification { Nil, Paths.None, PREventType.Defaults, + None, Map(), List( WorkflowJob( @@ -262,6 +270,7 @@ class GenerativePluginSpec extends Specification { Nil, Paths.None, PREventType.Defaults, + None, Map(), List( WorkflowJob( @@ -295,7 +304,7 @@ class GenerativePluginSpec extends Specification { |${" " * 2} |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.Include(List("**.scala", "**.sbt")), PREventType.Defaults, Map(), Nil, "sbt") mustEqual expected + compileWorkflow("test", List("main"), Nil, Paths.Include(List("**.scala", "**.sbt")), PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected } "render ignored paths on pull_request and push" in { @@ -314,7 +323,7 @@ class GenerativePluginSpec extends Specification { |${" " * 2} |""".stripMargin - compileWorkflow("test", List("main"), Nil, Paths.Ignore(List("docs/**")), PREventType.Defaults, Map(), Nil, "sbt") mustEqual expected + compileWorkflow("test", List("main"), Nil, Paths.Ignore(List("docs/**")), PREventType.Defaults, None, Map(), Nil, "sbt") mustEqual expected } } @@ -588,6 +597,58 @@ class GenerativePluginSpec extends Specification { - run: csbt ++$${{ matrix.scala }} ci-release""" } + "compile a job with specific permissions" in { + val results = compileJob( + WorkflowJob( + "publish", + "Publish Release", + List( + WorkflowStep.Sbt(List("ci-release"))), + permissions = Some( + Permissions.Specify(Map( + PermissionScope.IdToken -> PermissionValue.Write + )) + )), + "csbt") + + results mustEqual s"""publish: + name: Publish Release + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.6] + java: [temurin@11] + runs-on: $${{ matrix.os }} + permissions: + id-token: write + steps: + - run: csbt ++$${{ matrix.scala }} ci-release""" + } + + "compile a job with read-all permissions" in { + val results = compileJob( + WorkflowJob( + "publish", + "Publish Release", + List( + WorkflowStep.Sbt(List("ci-release"))), + permissions = Some(Permissions.ReadAll) + ), + "csbt") + + results mustEqual s"""publish: + name: Publish Release + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.13.6] + java: [temurin@11] + runs-on: $${{ matrix.os }} + permissions: read-all + steps: + - run: csbt ++$${{ matrix.scala }} ci-release""" + } + "compile a job with an environment containing a url" in { val results = compileJob( WorkflowJob(