Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions .github/workflows/generate-targeted-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Install xq
run: sudo apt install -y xq

- uses: guardian/setup-scala@v1
- name: Write a custom scala-steward configuration to target a specific artifact
env:
GH_TOKEN: ${{ github.token }}
TARGET_PR: ${{ inputs.target_pr }}
run: ./scripts/write-config-to-target-artefact.sh

- name: Setup Java
uses: actions/setup-java@v5
with:
java-version: "21"
distribution: "corretto"

TARGETED_RELEASES_GITHUB_APP_CLIENT_ID: 214238
TARGETED_RELEASES_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SCALA_STEWARD_APP_PRIVATE_KEY }}
run: sbt "run ${{ inputs.target_pr }} targeted-scala-steward.conf"
- name: Execute Scala Steward
uses: scala-steward-org/scala-steward-action@v2.77.0
with:
github-app-id: 214238
github-app-installation-id: 26822732
github-app-key: ${{ secrets.SCALA_STEWARD_APP_PRIVATE_KEY }}
repo-config: targeted-scala-steward.conf # from checkout of guardian/scala-steward-public-repos
repo-config: targeted-scala-steward.conf
other-args: "--add-labels"
21 changes: 20 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
.idea/
test-results/
.DS_Store

# sbt specific
.cache/
.history/
.lib/
.bsp/
.scala-build/
dist/*
target/
lib_managed/
src_managed/
project/boot/
project/plugins/project/

# Scala-IDE specific
.scala_dependencies
.worksheet
.idea
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
java corretto-21.0.3.9.1
9 changes: 9 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
scalaVersion := "3.3.6"
scalacOptions := Seq("-deprecation", "-release:21")

libraryDependencies ++= Seq(
"com.madgag.play-git-hub" %% "core" % "10.0.0",
"org.scalatest" %% "scalatest" % "3.2.19" % Test
)

Compile / run / fork := true
1 change: 1 addition & 0 deletions project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.11.7
50 changes: 0 additions & 50 deletions scripts/write-config-to-target-artefact.sh

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.gu.scalasteward.targeted

import cats.*
import cats.data.*
import cats.effect.{ExitCode, IO, Resource, ResourceApp}
import cats.implicits.*
import com.gu.scalasteward.targeted.model.{Artifact, Config}
import com.madgag.github.apps.GitHubAppJWTs
import com.madgag.scalagithub
import com.madgag.scalagithub.GitHub
import com.madgag.scalagithub.model.{Comment, PullRequest, RepoId}
import sttp.model.*

import java.nio.file.{Files, Path}
import scala.collection.immutable.SortedSet
import scala.concurrent.ExecutionContext.Implicits.global

object WriteConfigTargetingPreviewRelease extends ResourceApp {
def run(args: List[String]): Resource[IO, ExitCode] = {
val prId = PullRequest.Id.from(Uri.unsafeParse(args(0)))
val outputFile = Path.of(args(1))
for {
gitHubFactory <- GitHub.Factory()
clientWithAccess <- Resource.eval(gitHubFactory.accessSoleAppInstallation(GitHubAppJWTs.fromConfigMap(sys.env, "TARGETED_RELEASES")))
exitCode <- Resource.eval(createTargetedReleaseConfigFor(prId, outputFile)(using clientWithAccess.gitHub))
} yield exitCode
}

def createTargetedReleaseConfigFor(prId: PullRequest.Id, outputFile: Path)(using gitHub: GitHub): IO[ExitCode] = for {
pr <- summon[GitHub].getPullRequest(prId).map(_.result)
configOpt <- scalaStewardConfigForMostRecentPreviewReleaseOf(pr).value
exitCode <- IO {
configOpt.fold {
Console.err.println(s"Could not generate config for ${pr.html_url}")
ExitCode.Error
} { config =>
Files.writeString(outputFile, config.text)
println(s"Wrote config to ${outputFile.toAbsolutePath} :\n\n${config.text}")
ExitCode.Success
}
}
} yield exitCode

def scalaStewardConfigForMostRecentPreviewReleaseOf(pr: PullRequest)(using GitHub): OptionT[IO, Config] =
findLatestPreviewVersionIn(pr).flatMap(version => scalaStewardConfigFor(pr, version))

def findLatestPreviewVersionIn(pr: PullRequest)(using GitHub): OptionT[IO,String] =
OptionT(pr.comments2.list().filter(_.user.login == "gu-scala-library-release[bot]").map(findPreviewVersionIn).unNone.compile.last)

def findPreviewVersionIn(comment: Comment): Option[String] = comment.body.linesIterator.find(_.contains("-PREVIEW"))

def tagMessageFor(repoId: RepoId, version: String)(using g: GitHub): IO[String] = for {
_ <- IO.println(s"Looking for '${repoId.fullName}' release tag: $version")
repo <- g.getRepo(repoId).map(_.result)
ref <- repo.refs.get(s"tags/v$version").map(_.result)
tag <- repo.tags.get(ref.objectId.name).map(_.result)
} yield tag.message

def artifactsFrom(tagMessage: String): Seq[Artifact] =
tagMessage.linesIterator.filter(_.endsWith(".pom")).flatMap(Artifact.fromHashdeepPomLine).toSeq

def scalaStewardConfigFor(pr: PullRequest, version: String)(using GitHub): OptionT[IO, Config] = OptionT(for {
tagMessage <- tagMessageFor(pr.prId.repo, version)
} yield for {
artifacts <- NonEmptySet.fromSet(SortedSet.from(artifactsFrom(tagMessage)))
} yield Config(pr, artifacts.map(_.withoutScalaVersion)))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.gu.scalasteward.targeted.model

import cats.Order
import play.api.libs.json.{Json, OWrites}

object Artifact {
/**
* Parses a single line from the HashDeep (https://github.com/jessek/hashdeep) output included in
* the annotated Git Tag message for a release by our Scala Library Release workflow:
*
* https://github.com/guardian/gha-scala-library-release-workflow/blob/fa09703460a2eae0fd7d47b3f3088b34c400f973/actions/sign/action.yml#L106-L110
*
* An example line would look like this:
*
* `c96c2303d065496792e7edb379b0e671f57edbb13cecb6b2d94deb22d1a504d4 ./com/gu/panda-hmac-core_3/11.0.0/panda-hmac-core_3-11.0.0.pom`
*
* We only want to extract the groupId, artifactId & version (not the hash).
*/
def fromHashdeepPomLine(pomLine: String): Option[Artifact] = {
val segments = pomLine.split(' ').last.split('/').drop(1).dropRight(1).reverse.toList
segments match {
case version :: artifactId :: groupSegments =>
Some(Artifact(groupSegments.reverse.mkString("."), artifactId, version))
case _ => None
}
}

given Order[Artifact] = Order.by(Tuple.fromProductTyped) // required by NonEmptySet

given OWrites[Artifact] = Json.writes
}

case class Artifact(groupId: String, artifactId: String, version: String) {
val artifactIdWithoutScalaOrSbtVersion: String = artifactId.split('_').head

lazy val withoutScalaVersion: Artifact = copy(artifactId = artifactIdWithoutScalaOrSbtVersion)
}
30 changes: 30 additions & 0 deletions src/main/scala/com/gu/scalasteward/targeted/model/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.gu.scalasteward.targeted.model

import cats.data.NonEmptySet
import com.gu.scalasteward.targeted.model.Artifact
import com.madgag.scalagithub.model.PullRequest
import play.api.libs.json.Json.toJson

case class Config(
pr: PullRequest,
updates: NonEmptySet[Artifact]
) {
val commitsMessage: String = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"

val updatesJson: String = toJson(updates.toSortedSet.toSeq).toString

val text =
s"""
|updates.allow = $updatesJson
|
|updates.allowPreReleases = $updatesJson
|
|pullRequests.draft = true
|commits.message = "$commitsMessage"
|pullRequests.grouping = [{
| name = "${pr.prId.slug}",
| title = "Update to `${pr.prId.repo.name}` PR #${pr.number} (`${pr.head.ref}`)",
| filter = [{"group" = "*"}]
|}]
|""".stripMargin
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.gu.scalasteward.targeted.model

import org.scalatest.OptionValues
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class ArtifactTest extends AnyFlatSpec with Matchers with OptionValues {
it should "read artifact coordinates from a hashdeep line, as written by gha-scala-library-release-workflow" in {
Artifact.fromHashdeepPomLine("c96c2303d065496792e7edb379b0e671f57edbb13cecb6b2d94deb22d1a504d4 ./com/gu/panda-hmac-core_3/11.0.0/panda-hmac-core_3-11.0.0.pom").value shouldBe
Artifact("com.gu", "panda-hmac-core_3", "11.0.0")
}

it should "remove the Scala version from the artifact id" in {
Artifact("com.gu", "panda-hmac-core_3", "11.0.0").artifactIdWithoutScalaOrSbtVersion shouldBe "panda-hmac-core"
}

it should "remove the Scala & sbt version from a sbt plugin artifact" in {
Artifact("com.gu", "sbt-scrooge-typescript_2.12_1.0", "4.0.0").artifactIdWithoutScalaOrSbtVersion shouldBe "sbt-scrooge-typescript"
}
}