Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repo specific post update hooks #2434

Merged
10 changes: 10 additions & 0 deletions docs/repo-specific-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ scalafmt.runAfterUpgrading = false
# are specified using the buildRoots property. Note that the paths used there are relative and if the repo directory itself also contains a build.sbt the dot can be used to specify it.
# Default: ["."]
buildRoots = [ ".", "subfolder/projectA" ]

# Define commands that are executed after an update via a hook.
# A groupId and/or artifactId can be defined to only execute after certain dependencies are updated. If neither is defined, the hook runs for every update.
postUpdateHooks = [{
command = "sbt protobufGenerate",
useSandbox = true,
commitMessage = "Regenerated protobuf files",
groupId = "com.github.sbt",
artifiactId = "sbt-protobuf"
}]
```

The version information given in the patterns above can be in two formats:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ final class HookExecutor[F[_]](implicit
F: MonadThrow[F]
) {
def execPostUpdateHooks(data: RepoData, update: Update): F[List[EditAttempt]] =
HookExecutor.postUpdateHooks
(HookExecutor.postUpdateHooks ++ data.config.postUpdateHooks.map(_.toHook))
.filter { hook =>
update.groupId === hook.groupId &&
update.artifactIds.exists(_.name === hook.artifactId.name) &&
hook.groupId.forall(update.groupId === _) &&
hook.artifactId.forall(aid => update.artifactIds.exists(_.name === aid.name)) &&
hook.enabledByCache(data.cache) &&
hook.enabledByConfig(data.config)
}
Expand All @@ -51,7 +51,10 @@ final class HookExecutor[F[_]](implicit

private def execPostUpdateHook(repo: Repo, update: Update, hook: PostUpdateHook): F[EditAttempt] =
for {
_ <- logger.info(s"Executing post-update hook for ${hook.groupId}:${hook.artifactId.name}")
_ <- logger.info(
s"Executing post-update hook for ${update.groupId}:${update.mainArtifactId} with command " +
s"${hook.command.mkString_("'", " ", "'")}"
)
repoDir <- workspaceAlg.repoDir(repo)
result <- logger.attemptWarn.log("Post-update hook failed") {
processAlg.execMaybeSandboxed(hook.useSandbox)(hook.command, repoDir)
Expand Down Expand Up @@ -82,8 +85,8 @@ object HookExecutor {
enabledByCache: RepoCache => Boolean
): PostUpdateHook =
PostUpdateHook(
groupId = groupId,
artifactId = artifactId,
groupId = Some(groupId),
artifactId = Some(artifactId),
command = Nel.of("sbt", "githubWorkflowGenerate"),
useSandbox = true,
commitMessage = _ => CommitMsg("Regenerate workflow with sbt-github-actions"),
Expand All @@ -93,8 +96,8 @@ object HookExecutor {

private val scalafmtHook =
PostUpdateHook(
groupId = scalafmtGroupId,
artifactId = scalafmtArtifactId,
groupId = Some(scalafmtGroupId),
artifactId = Some(scalafmtArtifactId),
command = ScalafmtAlg.postUpdateHookCommand,
useSandbox = false,
commitMessage = update => CommitMsg(s"Reformat with scalafmt ${update.nextVersion}"),
Expand All @@ -104,8 +107,8 @@ object HookExecutor {

private val sbtJavaFormatterHook =
PostUpdateHook(
groupId = GroupId("com.lightbend.sbt"),
artifactId = ArtifactId("sbt-java-formatter"),
groupId = Some(GroupId("com.lightbend.sbt")),
artifactId = Some(ArtifactId("sbt-java-formatter")),
command = Nel.of("sbt", "javafmtAll"),
useSandbox = true,
commitMessage =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import org.scalasteward.core.repoconfig.RepoConfig
import org.scalasteward.core.util.Nel

final case class PostUpdateHook(
groupId: GroupId,
artifactId: ArtifactId,
groupId: Option[GroupId],
artifactId: Option[ArtifactId],
command: Nel[String],
useSandbox: Boolean,
commitMessage: Update => CommitMsg,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2018-2021 Scala Steward contributors
*
* 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.scalasteward.core.repoconfig

import io.circe.Codec
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredCodec
import org.scalasteward.core.data.{ArtifactId, GroupId}
import org.scalasteward.core.edit.hooks.PostUpdateHook
import org.scalasteward.core.git.CommitMsg
import org.scalasteward.core.util.Nel

final case class PostUpdateHookConfig(
groupId: Option[GroupId],
artifactId: Option[String],
command: String,
useSandbox: Boolean,
fthomas marked this conversation as resolved.
Show resolved Hide resolved
commitMessage: String
) {
def toHook: PostUpdateHook =
Nel
.fromList(command.split(' ').toList)
.fold(
throw new Exception("Post update hooks must have a command defined.")
fthomas marked this conversation as resolved.
Show resolved Hide resolved
) { cmd =>
PostUpdateHook(
groupId,
artifactId.map(ArtifactId(_)),
command = cmd,
useSandbox = useSandbox,
commitMessage = _ => CommitMsg(commitMessage),
enabledByCache = _ => true,
enabledByConfig = _ => true
)
}
}

object PostUpdateHookConfig {

implicit val postUpdateHooksConfiguration: Configuration =
Configuration.default.withDefaults

implicit val postUpdateHooksConfigCodec: Codec[PostUpdateHookConfig] =
deriveConfiguredCodec

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final case class RepoConfig(
pullRequests: PullRequestsConfig = PullRequestsConfig(),
scalafmt: ScalafmtConfig = ScalafmtConfig(),
updates: UpdatesConfig = UpdatesConfig(),
postUpdateHooks: List[PostUpdateHookConfig] = Nil,
updatePullRequests: Option[PullRequestUpdateStrategy] = None,
buildRoots: Option[List[BuildRootConfig]] = None
) {
Expand Down Expand Up @@ -68,6 +69,7 @@ object RepoConfig {
pullRequests = x.pullRequests |+| y.pullRequests,
scalafmt = x.scalafmt |+| y.scalafmt,
updates = x.updates |+| y.updates,
postUpdateHooks = x.postUpdateHooks ++ y.postUpdateHooks,
updatePullRequests = x.updatePullRequests.orElse(y.updatePullRequests),
buildRoots = x.buildRoots |+| y.buildRoots
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ class EditAlgTest extends FunSuite {
"VAR1=val1" :: "VAR2=val2" :: repoDir.toString :: scalafmtBinary :: opts.nonInteractive :: opts.modeChanged
),
Cmd(gitStatus(repoDir)),
Log("Executing post-update hook for org.scalameta:scalafmt-core"),
Log(
"Executing post-update hook for org.scalameta:scalafmt-core with command 'scalafmt --non-interactive'"
),
Cmd(
"VAR1=val1" :: "VAR2=val2" :: repoDir.toString :: scalafmtBinary :: opts.nonInteractive :: Nil
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.scalasteward.core.mock.MockConfig.gitCmd
import org.scalasteward.core.mock.MockContext.context.{hookExecutor, workspaceAlg}
import org.scalasteward.core.mock.MockState
import org.scalasteward.core.mock.MockState.TraceEntry.{Cmd, Log}
import org.scalasteward.core.repoconfig.{RepoConfig, ScalafmtConfig}
import org.scalasteward.core.repoconfig.{PostUpdateHookConfig, RepoConfig, ScalafmtConfig}
import org.scalasteward.core.scalafmt.ScalafmtAlg.opts
import org.scalasteward.core.scalafmt.{scalafmtArtifactId, scalafmtBinary, scalafmtGroupId}
import org.scalasteward.core.vcs.data.Repo
Expand Down Expand Up @@ -38,7 +38,9 @@ class HookExecutorTest extends CatsEffectSuite {

val expected = initial.copy(
trace = Vector(
Log("Executing post-update hook for org.scalameta:scalafmt-core"),
Log(
"Executing post-update hook for org.scalameta:scalafmt-core with command 'scalafmt --non-interactive'"
),
Cmd(
"VAR1=val1" :: "VAR2=val2" :: repoDir.toString :: scalafmtBinary :: opts.nonInteractive :: Nil
),
Expand Down Expand Up @@ -79,7 +81,9 @@ class HookExecutorTest extends CatsEffectSuite {

val expected = MockState.empty.copy(
trace = Vector(
Log("Executing post-update hook for com.codecommit:sbt-github-actions"),
Log(
"Executing post-update hook for com.codecommit:sbt-github-actions with command 'sbt githubWorkflowGenerate'"
),
Cmd(
repoDir.toString,
"firejail",
Expand All @@ -96,4 +100,40 @@ class HookExecutorTest extends CatsEffectSuite {

state.map(assertEquals(_, expected))
}

test("hook from config") {
val update = ("com.random".g % "cool-lib".a % "1.0" %> "1.1").single
val config = RepoConfig(
postUpdateHooks = List(
PostUpdateHookConfig(
groupId = None,
artifactId = None,
command = "sbt mySbtCommand",
useSandbox = true,
commitMessage = "Updated with a hook!"
)
)
)
val data = RepoData(repo, dummyRepoCache, config)
val state = hookExecutor.execPostUpdateHooks(data, update).runS(MockState.empty)

val expected = MockState.empty.copy(
trace = Vector(
Log("Executing post-update hook for com.random:cool-lib with command 'sbt mySbtCommand'"),
Cmd(
repoDir.toString,
"firejail",
"--quiet",
s"--whitelist=$repoDir",
"--env=VAR1=val1",
"--env=VAR2=val2",
"sbt",
"mySbtCommand"
),
Cmd(gitCmd(repoDir), "status", "--porcelain", "--untracked-files=no", "--ignore-submodules")
)
)

state.map(assertEquals(_, expected))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.scalasteward.core.mock.MockState.TraceEntry.Log
import org.scalasteward.core.mock.{MockConfig, MockState}
import org.scalasteward.core.util.Nel
import org.scalasteward.core.vcs.data.Repo

import scala.concurrent.duration._

class RepoConfigAlgTest extends FunSuite {
Expand Down Expand Up @@ -190,4 +191,53 @@ class RepoConfigAlgTest extends FunSuite {
RepoConfig(updates = UpdatesConfig(ignore = List(UpdatePattern("a".g, None, None))))
assertEquals(config, expected)
}

test("config with postUpdateHook without group and artifact id") {
val content =
"""|postUpdateHooks = [{
| command = "sbt mySbtCommand"
| useSandbox = false,
| commitMessage = "Updated with a hook!"
| }]
|""".stripMargin
val config = RepoConfigAlg.parseRepoConfig(content)
val expected = RepoConfig(
postUpdateHooks = List(
PostUpdateHookConfig(
groupId = None,
artifactId = None,
command = "sbt mySbtCommand",
useSandbox = false,
commitMessage = "Updated with a hook!"
)
)
)

assertEquals(config, Right(expected))
}

test("config with postUpdateHook with group and artifact id") {
val content =
"""|postUpdateHooks = [{
| groupId = "eu.timepit"
| artifactId = "refined.1"
| command = "sbt mySbtCommand"
| useSandbox = false,
| commitMessage = "Updated with a hook!"
| }]
|""".stripMargin
val config = RepoConfigAlg.parseRepoConfig(content)
val expected = RepoConfig(
postUpdateHooks = List(
PostUpdateHookConfig(
groupId = Some("eu.timepit".g),
artifactId = Some("refined.1"),
command = "sbt mySbtCommand",
useSandbox = false,
commitMessage = "Updated with a hook!"
)
)
)
assertEquals(config, Right(expected))
}
}