Skip to content

Commit 8eb02be

Browse files
committed
Use Scala script for targeted releases
1 parent 7087395 commit 8eb02be

File tree

3 files changed

+120
-60
lines changed

3 files changed

+120
-60
lines changed

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,20 @@ 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-
24-
- name: Write a custom scala-steward configuration to target a specific artifact
25-
env:
26-
GH_TOKEN: ${{ github.token }}
27-
TARGET_PR: ${{ inputs.target_pr }}
28-
run: ./scripts/write-config-to-target-artefact.sh
29-
3020
- name: Setup Java
3121
uses: actions/setup-java@v5
3222
with:
3323
java-version: "21"
3424
distribution: "corretto"
25+
- uses: coursier/cache-action@4e2615869d13561d626ed48655e1a39e5b192b3c
26+
- uses: VirtusLab/scala-cli-setup@77834b5926f3eb70869d8009530c65585f7a039b
27+
- name: Write a custom scala-steward configuration to target a specific artifact
28+
env:
29+
TARGETED_RELEASES_APP_CLIENT_ID: 214238
30+
TARGETED_RELEASES_APP_PRIVATE_KEY: ${{ secrets.SCALA_STEWARD_APP_PRIVATE_KEY }}
31+
run: |
32+
./scripts/write-config-to-target-artefact.scala ${{ inputs.target_pr }} targeted-scala-steward.conf
33+
cat targeted-scala-steward.conf
3534
3635
- name: Execute Scala Steward
3736
uses: scala-steward-org/scala-steward-action@v2.76.0
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env -S scala-cli shebang
2+
3+
//> using scala 3.3.6
4+
5+
//> using dep "com.madgag.play-git-hub::core:10.0.0-PREVIEW.examine-default-branch.2025-10-01T1252.5ed70273"
6+
7+
import cats.*
8+
import cats.data.*
9+
import cats.effect.unsafe.implicits.global
10+
import cats.effect.{ExitCode, IO, IOApp}
11+
import cats.implicits.*
12+
import cats.syntax.all.*
13+
import com.madgag.github.apps.GitHubAppAuth
14+
import com.madgag.scalagithub
15+
import com.madgag.scalagithub.GitHub
16+
import com.madgag.scalagithub.model.{Comment, PullRequest, RepoId}
17+
import play.api.libs.json.Json.toJson
18+
import play.api.libs.json.{Json, OWrites}
19+
import sttp.model.*
20+
21+
import java.nio.file.{Files, Path}
22+
import scala.collection.immutable.SortedSet
23+
import scala.concurrent.ExecutionContext.Implicits.global
24+
25+
val gitHubAppAuth: GitHubAppAuth = GitHubAppAuth.fromConfigMap(sys.env, "TARGETED_RELEASES")
26+
27+
case class Config(
28+
prId: PullRequest.Id,
29+
updates: NonEmptySet[Artifact]
30+
) {
31+
val groupIds: NonEmptySet[String] = updates.map(_.groupId)
32+
val versions: NonEmptySet[String] = updates.map(_.version)
33+
34+
val commitsMessage: String = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
35+
36+
val updatesJson: String = toJson(updates.toSortedSet.toSeq).toString
37+
38+
val text =
39+
s"""
40+
|updates.allow = $updatesJson
41+
|
42+
|updates.allowPreReleases = $updatesJson
43+
|
44+
|pullRequests.draft = true
45+
|commits.message = "$commitsMessage"
46+
|pullRequests.grouping = [{
47+
| name = "${prId.slug}",
48+
| title = "Update `${prId.repo.name}` to ${versions.toSortedSet.mkString(", ")}",
49+
| filter = [{"group" = "*"}]
50+
|}]
51+
|""".stripMargin
52+
}
53+
54+
case class Artifact(groupId: String, artifactId: String, version: String) {
55+
val artifactIdWithoutScalaVersion: String = artifactId.split('_').dropRight(1).mkString("_")
56+
57+
lazy val withoutScalaVersion: Artifact = copy(artifactId = artifactIdWithoutScalaVersion)
58+
}
59+
60+
object Artifact {
61+
// c96c2303d065496792e7edb379b0e671f57edbb13cecb6b2d94deb22d1a504d4 ./com/gu/panda-hmac-core_3/11.0.0/panda-hmac-core_3-11.0.0.pom
62+
def fromSha256PomLine(pomLine: String): Option[Artifact] = {
63+
val segments = pomLine.split(' ').last.split('/').drop(1).dropRight(1).reverse.toList
64+
segments match {
65+
case version :: artifactId :: groupSegments =>
66+
Some(Artifact(groupSegments.reverse.mkString("."), artifactId, version))
67+
case _ => None
68+
}
69+
}
70+
71+
given Order[Artifact] = Order.by(Tuple.fromProductTyped)
72+
73+
given OWrites[Artifact] = Json.writes
74+
}
75+
76+
object HelloWorld extends IOApp {
77+
78+
def run(args: List[String]) = (for {
79+
installationAccess <- gitHubAppAuth.accessSoleInstallation()
80+
_ <- IO.println(s"Hello, ${installationAccess.installedOnAccount.atLogin}!")
81+
prId = PullRequest.Id.from(Uri.unsafeParse(args(0)))
82+
outputFile = Path.of(args(1))
83+
given GitHub = installationAccess.accountAccess().gitHub
84+
pr <- summon[GitHub].getPullRequest(prId).map(_.result)
85+
configOpt <- scalaStewardConfigForMostRecentPreviewReleaseOf(pr).value
86+
_ <- IO(Files.writeString(outputFile, configOpt.map(_.text).mkString))
87+
} yield println(s"Wrote config to ${outputFile.toAbsolutePath}")).as(ExitCode.Success)
88+
89+
def scalaStewardConfigForMostRecentPreviewReleaseOf(pr: PullRequest)(using GitHub): OptionT[IO, Config] =
90+
findLatestPreviewVersionIn(pr).flatMap(version => scalaStewardConfigFor(pr.prId, version))
91+
92+
def findLatestPreviewVersionIn(pr: PullRequest)(using GitHub): OptionT[IO,String] =
93+
OptionT(pr.comments2.list().filter(_.user.login == "gu-scala-library-release[bot]").map(findPreviewVersionIn).unNone.compile.last)
94+
95+
def findPreviewVersionIn(comment: Comment): Option[String] = comment.body.linesIterator.find(_.contains("-PREVIEW"))
96+
97+
def tagMessageFor(repoId: RepoId, version: String)(using g: GitHub): IO[String] = for {
98+
repo <- g.getRepo(repoId).map(_.result)
99+
ref <- repo.refs.get(s"tags/v$version").map(_.result)
100+
tag <- repo.tags.get(ref.objectId.name).map(_.result)
101+
} yield tag.message
102+
103+
def artifactsFrom(tagMessage: String): Seq[Artifact] =
104+
tagMessage.linesIterator.filter(_.endsWith(".pom")).flatMap(Artifact.fromSha256PomLine).toSeq
105+
106+
def scalaStewardConfigFor(prId: PullRequest.Id, version: String)(using GitHub): OptionT[IO, Config] = OptionT(for {
107+
tagMessage <- tagMessageFor(prId.repo, version)
108+
} yield for {
109+
artifacts <- NonEmptySet.fromSet(SortedSet.from(artifactsFrom(tagMessage)))
110+
} yield Config(prId, artifacts.map(_.withoutScalaVersion)))
111+
}

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

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)