Skip to content

Commit b656f1b

Browse files
committed
Use Scala script for targeted releases
Give a warning when we can't generate config - probably because no preview is available
1 parent 608b2f1 commit b656f1b

File tree

10 files changed

+190
-66
lines changed

10 files changed

+190
-66
lines changed

.github/workflows/generate-targeted-prs.yml

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,17 @@ jobs:
1717
runs-on: ubuntu-latest
1818
steps:
1919
- uses: actions/checkout@v5
20-
21-
- name: Install xq
22-
run: sudo apt install -y xq
23-
20+
- uses: guardian/setup-scala@v1
2421
- name: Write a custom scala-steward configuration to target a specific artifact
2522
env:
26-
GH_TOKEN: ${{ github.token }}
27-
TARGET_PR: ${{ inputs.target_pr }}
28-
run: ./scripts/write-config-to-target-artefact.sh
29-
30-
- name: Setup Java
31-
uses: actions/setup-java@v5
32-
with:
33-
java-version: "21"
34-
distribution: "corretto"
35-
23+
TARGETED_RELEASES_GITHUB_APP_CLIENT_ID: 214238
24+
TARGETED_RELEASES_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SCALA_STEWARD_APP_PRIVATE_KEY }}
25+
run: sbt "run ${{ inputs.target_pr }} targeted-scala-steward.conf"
3626
- name: Execute Scala Steward
3727
uses: scala-steward-org/scala-steward-action@v2.77.0
3828
with:
3929
github-app-id: 214238
4030
github-app-installation-id: 26822732
4131
github-app-key: ${{ secrets.SCALA_STEWARD_APP_PRIVATE_KEY }}
42-
repo-config: targeted-scala-steward.conf # from checkout of guardian/scala-steward-public-repos
32+
repo-config: targeted-scala-steward.conf
4333
other-args: "--add-labels"

.gitignore

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,20 @@
1-
.idea/
1+
test-results/
2+
.DS_Store
3+
4+
# sbt specific
5+
.cache/
6+
.history/
7+
.lib/
8+
.bsp/
9+
.scala-build/
10+
dist/*
11+
target/
12+
lib_managed/
13+
src_managed/
14+
project/boot/
15+
project/plugins/project/
16+
17+
# Scala-IDE specific
18+
.scala_dependencies
19+
.worksheet
20+
.idea

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
java corretto-21.0.3.9.1

build.sbt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
scalaVersion := "3.3.6"
2+
scalacOptions := Seq("-deprecation", "-release:21")
3+
4+
libraryDependencies ++= Seq(
5+
"com.madgag.play-git-hub" %% "core" % "10.0.0",
6+
"org.scalatest" %% "scalatest" % "3.2.19" % Test
7+
)
8+
9+
Compile / run / fork := true

project/build.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=1.11.7

scripts/write-config-to-target-artefact.sh

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.gu.scalasteward.targeted
2+
3+
import cats.*
4+
import cats.data.*
5+
import cats.effect.{ExitCode, IO, Resource, ResourceApp}
6+
import cats.implicits.*
7+
import com.gu.scalasteward.targeted.model.{Artifact, Config}
8+
import com.madgag.github.apps.GitHubAppJWTs
9+
import com.madgag.scalagithub
10+
import com.madgag.scalagithub.GitHub
11+
import com.madgag.scalagithub.model.{Comment, PullRequest, RepoId}
12+
import sttp.model.*
13+
14+
import java.nio.file.{Files, Path}
15+
import scala.collection.immutable.SortedSet
16+
import scala.concurrent.ExecutionContext.Implicits.global
17+
18+
object WriteConfigTargetingPreviewRelease extends ResourceApp {
19+
def run(args: List[String]): Resource[IO, ExitCode] = {
20+
val prId = PullRequest.Id.from(Uri.unsafeParse(args(0)))
21+
val outputFile = Path.of(args(1))
22+
for {
23+
gitHubFactory <- GitHub.Factory()
24+
clientWithAccess <- Resource.eval(gitHubFactory.accessSoleAppInstallation(GitHubAppJWTs.fromConfigMap(sys.env, "TARGETED_RELEASES")))
25+
exitCode <- Resource.eval(createTargetedReleaseConfigFor(prId, outputFile)(using clientWithAccess.gitHub))
26+
} yield exitCode
27+
}
28+
29+
def createTargetedReleaseConfigFor(prId: PullRequest.Id, outputFile: Path)(using gitHub: GitHub): IO[ExitCode] = for {
30+
pr <- summon[GitHub].getPullRequest(prId).map(_.result)
31+
configOpt <- scalaStewardConfigForMostRecentPreviewReleaseOf(pr).value
32+
exitCode <- IO {
33+
configOpt.fold {
34+
Console.err.println(s"Could not generate config for ${pr.html_url}")
35+
ExitCode.Error
36+
} { config =>
37+
Files.writeString(outputFile, config.text)
38+
println(s"Wrote config to ${outputFile.toAbsolutePath} :\n\n${config.text}")
39+
ExitCode.Success
40+
}
41+
}
42+
} yield exitCode
43+
44+
def scalaStewardConfigForMostRecentPreviewReleaseOf(pr: PullRequest)(using GitHub): OptionT[IO, Config] =
45+
findLatestPreviewVersionIn(pr).flatMap(version => scalaStewardConfigFor(pr, version))
46+
47+
def findLatestPreviewVersionIn(pr: PullRequest)(using GitHub): OptionT[IO,String] =
48+
OptionT(pr.comments2.list().filter(_.user.login == "gu-scala-library-release[bot]").map(findPreviewVersionIn).unNone.compile.last)
49+
50+
def findPreviewVersionIn(comment: Comment): Option[String] = comment.body.linesIterator.find(_.contains("-PREVIEW"))
51+
52+
def tagMessageFor(repoId: RepoId, version: String)(using g: GitHub): IO[String] = for {
53+
_ <- IO.println(s"Looking for '${repoId.fullName}' release tag: $version")
54+
repo <- g.getRepo(repoId).map(_.result)
55+
ref <- repo.refs.get(s"tags/v$version").map(_.result)
56+
tag <- repo.tags.get(ref.objectId.name).map(_.result)
57+
} yield tag.message
58+
59+
def artifactsFrom(tagMessage: String): Seq[Artifact] =
60+
tagMessage.linesIterator.filter(_.endsWith(".pom")).flatMap(Artifact.fromHashdeepPomLine).toSeq
61+
62+
def scalaStewardConfigFor(pr: PullRequest, version: String)(using GitHub): OptionT[IO, Config] = OptionT(for {
63+
tagMessage <- tagMessageFor(pr.prId.repo, version)
64+
} yield for {
65+
artifacts <- NonEmptySet.fromSet(SortedSet.from(artifactsFrom(tagMessage)))
66+
} yield Config(pr, artifacts.map(_.withoutScalaVersion)))
67+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.gu.scalasteward.targeted.model
2+
3+
import cats.Order
4+
import play.api.libs.json.{Json, OWrites}
5+
6+
object Artifact {
7+
/**
8+
* Parses a single line from the HashDeep (https://github.com/jessek/hashdeep) output included in
9+
* the annotated Git Tag message for a release by our Scala Library Release workflow:
10+
*
11+
* https://github.com/guardian/gha-scala-library-release-workflow/blob/fa09703460a2eae0fd7d47b3f3088b34c400f973/actions/sign/action.yml#L106-L110
12+
*
13+
* An example line would look like this:
14+
*
15+
* `c96c2303d065496792e7edb379b0e671f57edbb13cecb6b2d94deb22d1a504d4 ./com/gu/panda-hmac-core_3/11.0.0/panda-hmac-core_3-11.0.0.pom`
16+
*
17+
* We only want to extract the groupId, artifactId & version (not the hash).
18+
*/
19+
def fromHashdeepPomLine(pomLine: String): Option[Artifact] = {
20+
val segments = pomLine.split(' ').last.split('/').drop(1).dropRight(1).reverse.toList
21+
segments match {
22+
case version :: artifactId :: groupSegments =>
23+
Some(Artifact(groupSegments.reverse.mkString("."), artifactId, version))
24+
case _ => None
25+
}
26+
}
27+
28+
given Order[Artifact] = Order.by(Tuple.fromProductTyped) // required by NonEmptySet
29+
30+
given OWrites[Artifact] = Json.writes
31+
}
32+
33+
case class Artifact(groupId: String, artifactId: String, version: String) {
34+
val artifactIdWithoutScalaOrSbtVersion: String = artifactId.split('_').head
35+
36+
lazy val withoutScalaVersion: Artifact = copy(artifactId = artifactIdWithoutScalaOrSbtVersion)
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.gu.scalasteward.targeted.model
2+
3+
import cats.data.NonEmptySet
4+
import com.gu.scalasteward.targeted.model.Artifact
5+
import com.madgag.scalagithub.model.PullRequest
6+
import play.api.libs.json.Json.toJson
7+
8+
case class Config(
9+
pr: PullRequest,
10+
updates: NonEmptySet[Artifact]
11+
) {
12+
val commitsMessage: String = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
13+
14+
val updatesJson: String = toJson(updates.toSortedSet.toSeq).toString
15+
16+
val text =
17+
s"""
18+
|updates.allow = $updatesJson
19+
|
20+
|updates.allowPreReleases = $updatesJson
21+
|
22+
|pullRequests.draft = true
23+
|commits.message = "$commitsMessage"
24+
|pullRequests.grouping = [{
25+
| name = "${pr.prId.slug}",
26+
| title = "Update to `${pr.prId.repo.name}` PR #${pr.number} (`${pr.head.ref}`)",
27+
| filter = [{"group" = "*"}]
28+
|}]
29+
|""".stripMargin
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.gu.scalasteward.targeted.model
2+
3+
import org.scalatest.OptionValues
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import org.scalatest.matchers.should.Matchers
6+
7+
class ArtifactTest extends AnyFlatSpec with Matchers with OptionValues {
8+
it should "read artifact coordinates from a hashdeep line, as written by gha-scala-library-release-workflow" in {
9+
Artifact.fromHashdeepPomLine("c96c2303d065496792e7edb379b0e671f57edbb13cecb6b2d94deb22d1a504d4 ./com/gu/panda-hmac-core_3/11.0.0/panda-hmac-core_3-11.0.0.pom").value shouldBe
10+
Artifact("com.gu", "panda-hmac-core_3", "11.0.0")
11+
}
12+
13+
it should "remove the Scala version from the artifact id" in {
14+
Artifact("com.gu", "panda-hmac-core_3", "11.0.0").artifactIdWithoutScalaOrSbtVersion shouldBe "panda-hmac-core"
15+
}
16+
17+
it should "remove the Scala & sbt version from a sbt plugin artifact" in {
18+
Artifact("com.gu", "sbt-scrooge-typescript_2.12_1.0", "4.0.0").artifactIdWithoutScalaOrSbtVersion shouldBe "sbt-scrooge-typescript"
19+
}
20+
}

0 commit comments

Comments
 (0)