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

Add marking PRs to release #437

Merged
merged 1 commit into from
Mar 20, 2021
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
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ jobs:
CLIENT_GITHUB_STORE_PASS: ${{ secrets.CLIENT_GITHUB_STORE_PASS }}
PLAY_PUBLISHER_CREDENTIALS: ${{ secrets.PLAY_PUBLISHER_CREDENTIALS }}
run: ./gradlew publishBundle --stacktrace
- name: Mark PRs with Milestone
env:
GITHUB_CLIENT_MIXPANEL_API_KEY: ${{ secrets.MIXPANEL_KEY }}
TOKEN_GITHUB_API: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew markAllPrsWithReleaseMilestone --stacktrace
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ android {
applicationId "com.jraska.github.client"
minSdkVersion 24
targetSdkVersion 30
versionName '0.22.1'
versionCode 86
versionName '0.23.0'
versionCode 87
multiDexEnabled true

testInstrumentationRunner "com.jraska.github.client.TestRunner"
Expand Down
6 changes: 6 additions & 0 deletions plugins/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ buildscript {

apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'
apply from: '../dependencies.gradle'

repositories {
jcenter()
Expand All @@ -19,6 +20,11 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.31"
implementation 'com.mixpanel:mixpanel-java:1.4.4'

implementation project.ext.retrofit
implementation project.ext.retrofitGsonConverter
implementation project.ext.okHttp
implementation project.ext.okHttpLoggingInterceptor

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.assertj:assertj-core:3.19.0'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.jraska.github.client.release

interface GitHubApi {
fun createMilestone(title: String): Int

fun setMilestoneBody(milestoneNumber: Int, body: String)

fun commentPr(prNumber: Int, body: String)

fun assignMilestone(prNumber: Int, milestoneNumber: Int)

fun setReleaseBody(release: String, body: String)

fun listPrsWithoutMilestone(): List<PullRequest>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jraska.github.client.release

class NotesComposer {
fun releaseNotes(pullRequests: List<PullRequest>): String {
return pullRequests.joinToString(separator = " \r\n", transform = { pr -> "#${pr.number}: ${pr.title}" })
}

fun prReleaseComment(release: Release): String {
return "This PR was released with [${release.releaseName}](${release.releaseUrl})"
}
}
13 changes: 13 additions & 0 deletions plugins/src/main/java/com/jraska/github/client/release/Release.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.jraska.github.client.release

import okhttp3.HttpUrl

class Release(
val releaseName: String,
val releaseUrl: HttpUrl
)

class PullRequest(
val number: Int,
val title: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.jraska.github.client.release

class ReleaseMarker(
private val gitHubApi: GitHubApi,
private val notesComposer: NotesComposer
) {
fun markPrsWithMilestone(release: Release) {
val pullRequests = gitHubApi.listPrsWithoutMilestone()

val milestoneNumber = gitHubApi.createMilestone(release.releaseName)

pullRequests.forEach { pr ->
gitHubApi.assignMilestone(pr.number, milestoneNumber)

val commentBody = notesComposer.prReleaseComment(release)
gitHubApi.commentPr(pr.number, commentBody)
}

val releaseNotes = notesComposer.releaseNotes(pullRequests)
gitHubApi.setMilestoneBody(milestoneNumber, releaseNotes)
gitHubApi.setReleaseBody(release.releaseName, releaseNotes)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.jraska.github.client.release

import com.jraska.github.client.release.data.GitHubApiFactory
import okhttp3.HttpUrl.Companion.toHttpUrl

object ReleaseMarksPRs {
fun execute(tag: String) {
val release = Release(tag, "https://github.com/jraska/github-client/releases/tag/$tag".toHttpUrl())

val api = GitHubApiFactory.create()
val releaseMarker = ReleaseMarker(api, NotesComposer())

releaseMarker.markPrsWithMilestone(release)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.jraska.github.client.release

import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.ByteArrayOutputStream
import java.io.File

class ReleasePlugin : Plugin<Project> {
Expand All @@ -17,6 +18,23 @@ class ReleasePlugin : Plugin<Project> {
updatePatchVersionInBuildGradle(project)
}
}

project.tasks.register("markAllPrsWithReleaseMilestone") {
it.doFirst {
val latestTag = project.latestTag()
ReleaseMarksPRs.execute(latestTag)
}
}
}

private fun Project.latestTag(): String {
val byteArrayOutputStream = ByteArrayOutputStream()
exec {
it.commandLine("git describe --tags --abbrev=0".split(" "))
it.standardOutput = byteArrayOutputStream
}

return byteArrayOutputStream.toString()
}

private fun updatePatchVersionInBuildGradle(project: Project) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.jraska.github.client.release.data

import com.jraska.github.client.release.GitHubApi
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.lang.IllegalStateException

object GitHubApiFactory {
fun create(): GitHubApi {
val apiToken = System.getenv("TOKEN_GITHUB_API") ?: throw IllegalStateException("GitHub API token missing")

val client = OkHttpClient.Builder()
.addInterceptor(GitHubInterceptor(apiToken))
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC))
.build()

val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/repos/jraska/github-client/")
.client(client)
.validateEagerly(true)
.addConverterFactory(GsonConverterFactory.create())
.build()

return GitHubApiImpl(retrofit.create(RetrofitGitHubApi::class.java))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.jraska.github.client.release.data

import com.jraska.github.client.release.GitHubApi
import com.jraska.github.client.release.PullRequest

class GitHubApiImpl(
private val api: RetrofitGitHubApi
) : GitHubApi {
override fun createMilestone(title: String): Int {
return api.createMilestone(MilestoneDto(title)).execute().body()!!.number
}

override fun setMilestoneBody(milestoneNumber: Int, body: String) {
api.updateMilestone(milestoneNumber, UpdateMilestoneDto(body)).execute()
}

override fun commentPr(prNumber: Int, body: String) {
api.sendComment(prNumber, CommentRequestDto(body)).execute()
}

override fun assignMilestone(prNumber: Int, milestoneNumber: Int) {
api.assignMilestone(prNumber, AssignMilestoneDto(milestoneNumber)).execute()
}

override fun setReleaseBody(release: String, body: String) {
val releaseId = api.getRelease(release).execute().body()!!.id
println(releaseId)

api.setReleseBody(releaseId, ReleaseBodyDto(body)).execute()
}

override fun listPrsWithoutMilestone(): List<PullRequest> {
val pulls = mutableListOf<PullRequest>()

var page = 1
do {
val previousSize = pulls.size

api.getPulls(page)
.execute()
.body()!!
.filter { it.milestone == null }
.map { PullRequest(it.number, it.title) }
.also { pulls.addAll(it) }
page++
} while (previousSize != pulls.size)

return pulls
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.jraska.github.client.release.data

import okhttp3.Interceptor
import okhttp3.Response

class GitHubInterceptor(
private val apiToken: String
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", "token $apiToken")
.addHeader("Accept", "application/vnd.github.v3+json")
.build()

return chain.proceed(newRequest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.jraska.github.client.release.data

import com.google.gson.annotations.SerializedName
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query

interface RetrofitGitHubApi {
@POST("milestones")
fun createMilestone(@Body dto: MilestoneDto): Call<CreateMilestoneResponseDto>

@PATCH("milestones/{milestoneNumber}")
fun updateMilestone(@Path("milestoneNumber") milestoneNumber: Int, @Body dto: UpdateMilestoneDto): Call<ResponseBody>

@POST("issues/{prNumber}/comments")
fun sendComment(@Path("prNumber") prNumber: Int, @Body commentRequestDto: CommentRequestDto): Call<ResponseBody>

@GET("releases/tags/{tag}")
fun getRelease(@Path("tag") tag: String): Call<ReleaseDto>

@PATCH("releases/{release_id}")
fun setReleseBody(@Path("release_id") id: Int, @Body relaseBody: ReleaseBodyDto): Call<ResponseBody>

@PATCH("issues/{issue_number}")
fun assignMilestone(@Path("issue_number") prNumber: Int, @Body dto: AssignMilestoneDto): Call<ResponseBody>

@GET("pulls?state=closed&base=master&per_page=100")
fun getPulls(@Query("page") page: Int = 1): Call<List<PullRequestDto>>
}

class PullRequestDto {
@SerializedName("number")
var number: Int = 0

@SerializedName("title")
var title: String = ""

@SerializedName("milestone")
var milestone: MilestoneDto? = null
}

class ReleaseBodyDto(
@SerializedName("body")
val body: String
)

class ReleaseDto {
@SerializedName("id")
var id: Int = 0
}

class AssignMilestoneDto(
@SerializedName("milestone")
val milestoneNumber: Int
)

class MilestoneDto(
@SerializedName("title")
val title: String,

@SerializedName("state")
val state: String = "closed"
)

class UpdateMilestoneDto(
@SerializedName("description")
val body: String,
)

class CreateMilestoneResponseDto {
@SerializedName("number")
var number: Int = 0
}

class CommentRequestDto(val body: String)