diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ecac5a0..c42cb53f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Container modules occupy too much disk space. The GitHub-hosted runners ran into the # error: "no space left on device." @@ -36,12 +36,15 @@ jobs: swap-storage: false - name: Setup Java - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: distribution: temurin # See 'Supported distributions' for available options java-version: ${{ matrix.java }} check-latest: true - + cache: 'sbt' + - uses: sbt/setup-sbt@v1 + with: + sbt-runner-version: 1.10.5 - name: Restore cache uses: actions/cache@v2 with: @@ -53,4 +56,4 @@ jobs: restore-keys: | cache-v1- - name: Compile and Test - run: sbt +test \ No newline at end of file + run: sbt +test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..32dffb43 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# CONTRIBUTING + +## How to release + +### Prerequisites +- Install [sdkman](https://sdkman.io/install) +- Install latest scala, e.g. `sdk install scala 3.6.2` + +### Generate release notes + +- Bump the version in `version.sbt` +- Run `git tag v` (e.g., `git tag v0.42.3`) +- release notes with `scala bin/generate-release.scala -- release-notes -s v -e v --token GITHUB_TOKEN` (e.g., `scala bin/generate-release.scala -- release-notes -s v0.42.2 -e v0.42.3 --token GITHUB_TOKEN`). + diff --git a/bin/generate-release.scala b/bin/generate-release.scala new file mode 100644 index 00000000..43bb24f5 --- /dev/null +++ b/bin/generate-release.scala @@ -0,0 +1,195 @@ +//> using scala 3.6.2 +//> using dep "com.lihaoyi::os-lib:0.11.3" +//> using dep "com.monovore::decline:2.4.1" +//> using dep "org.kohsuke:github-api:1.326" + +import scala.jdk.CollectionConverters._ +import scala.collection.mutable.ListBuffer +import scala.collection.mutable +import org.kohsuke.github.GitHubBuilder +import com.monovore.decline.* +import cats.syntax.all.given + +import java.util.Date +import java.text.SimpleDateFormat + +object ReleaseNotes { + val Organization = "testcontainers" + val Repo = "testcontainers-scala" + + val firstTagOpt = + Opts.option[String](long = "first-tag", help = "start git tag", short = "s") + + val lastTagOpt = + Opts.option[String](long = "last-tag", help = "end git tag", short = "e") + + val githubTokenOpt = + Opts.option[String](long = "token", help = "github token", short = "t") + val debugOpt = Opts + .flag(long = "debug", help = "prints debug information", short = "d") + .orFalse + + final case class Arguments( + firstTag: String, + lastTag: String, + githubToken: String, + debug: Boolean + ) + + val opts: Opts[Arguments] = + Opts.subcommand("release-notes", "generates release notes") { + (firstTagOpt, lastTagOpt, githubTokenOpt, debugOpt).mapN(Arguments.apply) + } + + def run(args: Arguments): Unit = { + def debug(msg: String) = if (args.debug) + println(msg) + + val commits = os + .proc(List("git", "rev-list", s"${args.firstTag}..${args.lastTag}")) + .call() + .out + .trim() + .linesIterator + .size + + debug( + s"Number of commits between ${args.firstTag}..${args.lastTag}: $commits" + ) + + val contributors = os + .proc( + List( + "git", + "shortlog", + "-sn", + "--no-merges", + s"${args.firstTag}..${args.lastTag}" + ) + ) + .call() + .out + .trim() + .linesIterator + .toList + + val command = List( + "git", + "log", + s"${args.firstTag}..${args.lastTag}", + "--first-parent", + "master", + "--pretty=format:%H" + ) + + val output = os.proc(command).call().out.trim() + + val gh = new GitHubBuilder() + .withOAuthToken(args.githubToken) + .withEndpoint("https://api.github.com") + .build() + + val foundPRs = mutable.Set.empty[Int] + val mergedPRs = ListBuffer[String]() + + for { + // group in order to optimize API + searchSha <- + output.split('\n').grouped(5).map(_.mkString("SHA ", " SHA ", "")) + allMatching = + gh.searchIssues() + .q(s"repo:$Organization/$Repo type:pr $searchSha") + .list() + pr <- allMatching.toList().asScala.sortBy(_.getClosedAt()).reverse + prNumber = pr.getNumber() + if !foundPRs(prNumber) + } { + foundPRs += prNumber + val login = pr.getUser().getLogin() + val formattedPR = + s"- ${pr.getTitle()} [\\#${pr.getNumber()}](${pr.getHtmlUrl()}) ([$login](https://github.com/$login))" + mergedPRs += formattedPR + } + + val releaseNotes = + template( + firstTag = args.firstTag, + lastTag = args.lastTag, + mergedPrs = mergedPRs.toList, + commits = commits, + contributors = contributors + ) + + debug(releaseNotes) + + gh.getRepository(s"$Organization/$Repo") + .createRelease(args.lastTag) + .name(s"${args.lastTag}") + .body(releaseNotes) + .create() + } + + def today: String = { + val formatter = new SimpleDateFormat("yyyy-MM-dd"); + formatter.format(new Date()); + } + + def template( + firstTag: String, + lastTag: String, + mergedPrs: List[String], + commits: Int, + contributors: List[String] + ): String = + s""" + |## testcontainers-scala $lastTag + | + |We're happy to announce the release of test-containers $lastTag, which + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
Commits since last release$commits
Merged PRs${mergedPrs.size}
Contributors${contributors.size}
+ | + |## Contributors + | + |Big thanks to everybody who contributed to this release or reported an issue! + | + |``` + |$$ git shortlog -sn --no-merges $firstTag..$lastTag + |${contributors.mkString("\n")} + |``` + | + |## Merged PRs + | + |## [$lastTag](https://github.com/$Organization/$Repo/tree/$lastTag) (${today}) + | + |[Full Changelog](https://github.com/$Organization/$Repo/compare/$firstTag...$lastTag) + | + |**Merged pull requests:** + | + |${mergedPrs.mkString("\n")} + |""".stripMargin +} + +object GenerateRelease + extends CommandApp( + name = "generate-release", + header = "helpers to automate releases", + version = "0.0.0", + main = (ReleaseNotes.opts).map { case args: ReleaseNotes.Arguments => + ReleaseNotes.run(args) + } + )