Skip to content

Commit

Permalink
ci: Copy properties from issue to pull request (#1310)
Browse files Browse the repository at this point in the history
* ci: Copy properties from issue to pull request

* Clean up code

* add some tests and refactor mock server

* added tests

* added github job

* added documentation

* update documentation and action

* update tests

* change for PR

* change for PR

* Update flank-scripts/src/main/kotlin/flank/scripts/pullrequest/GitHubIssuePropertiesCopy.kt

Co-authored-by: pawelpasterz <[email protected]>

* fix setting assignees

* fixed test

* add tests

* Update GithubMockServerHandler.kt

Co-authored-by: pawelpasterz <[email protected]>
  • Loading branch information
piotradamczyk5 and pawelpasterz authored Nov 20, 2020
1 parent 0c87150 commit ac5a2a8
Show file tree
Hide file tree
Showing 24 changed files with 700 additions and 100 deletions.
13 changes: 13 additions & 0 deletions .github/workflows/pr_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@ on:
- synchronize

jobs:
copy_properties:
runs-on: macos-latest
if: ${{ github.event.action == 'opened' }}
steps:
- uses: actions/checkout@v2
- name: Gradle Build flankScripts and add it to PATH
run: |
./flank-scripts/bash/buildFlankScripts.sh
echo "./flank-scripts/bash" >> $GITHUB_PATH
- name: Copy properties
run: flankScripts pullRequest copyProperties --github-token=${{ secrets.GITHUB_TOKEN }} --zenhub-token=${{ secrets.ZENHUB_API_KEY }} --pr-number=${{ github.event.number }}
check_title:
runs-on: macos-latest
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ secrets.GITHUB_TOKEN }}

- uses: amannn/[email protected]
if: github.event.pull_request.draft == false
env:
Expand Down
20 changes: 20 additions & 0 deletions flank-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,23 @@ Update binaries used by Flank

#### `buildFlank`
Build Flank test runner

### pullRequest
To show all available commands for pullRequest use:
`flankScripts pullRequest`

Available commands are:
- `copyProperties` Copy properties from referenced issue to pull request

#### `copyProperties`
Command to copy labels, assignees and estimate are copied from source issue.
Source issue is discovered by:
- Having reference in description (ex. Fixes #1092)
or
- Having reference in branch name(ex. #1092_copy_properties_to_pull_request)

| Option | Description |
|----------------|---------------------|
| --github-token | GitHub Token |
| --zenhub-token | ZenHub api Token |
| --pr-number | Pull request number |
4 changes: 3 additions & 1 deletion flank-scripts/src/main/kotlin/flank/scripts/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import flank.scripts.ci.CiCommand
import flank.scripts.dependencies.DependenciesCommand
import flank.scripts.pullrequest.PullRequestCommand
import flank.scripts.release.ReleaseCommand
import flank.scripts.shell.ShellCommand
import flank.scripts.testartifacts.TestArtifactsCommand
Expand All @@ -19,6 +20,7 @@ fun main(args: Array<String>) {
CiCommand(),
DependenciesCommand,
TestArtifactsCommand(),
ShellCommand
ShellCommand,
PullRequestCommand
).main(args)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package flank.scripts.ci.releasenotes
import com.github.kittinunf.result.Result
import com.github.kittinunf.result.getOrElse
import com.github.kittinunf.result.map
import flank.scripts.github.GithubPullRequest
import flank.scripts.github.GithubUser
import flank.scripts.github.getLatestReleaseTag
import flank.scripts.github.getPrDetailsByCommit
import flank.scripts.utils.markdownLink
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import flank.scripts.release.updatebugsnag.BugSnagResponse
sealed class FlankScriptsExceptions : Exception()

class GitHubException(val body: GitHubErrorResponse) : FlankScriptsExceptions() {

override val message: String?
get() = toString()

override fun toString(): String {
return "Error while doing GitHub request, because of ${body.message}, more info at ${body.documentationUrl}"
}
Expand Down
26 changes: 20 additions & 6 deletions flank-scripts/src/main/kotlin/flank/scripts/github/GithubApi.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package flank.scripts.github

import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.core.Request
import com.github.kittinunf.fuel.core.extensions.authentication
import com.github.kittinunf.fuel.coroutines.awaitResult
import com.jcabi.github.Coordinates
import com.jcabi.github.Release
import com.jcabi.github.Releases
import com.jcabi.github.Repo
import com.jcabi.github.RtGithub
import flank.scripts.ci.releasenotes.GithubPullRequestDeserializer
import flank.scripts.ci.releasenotes.GithubReleaseDeserializable
import flank.scripts.exceptions.mapClientError
import flank.scripts.exceptions.toGithubException
Expand All @@ -17,15 +17,13 @@ import flank.scripts.exceptions.toGithubException

suspend fun getPrDetailsByCommit(commitSha: String, githubToken: String) =
Fuel.get("https://api.github.com/repos/flank/flank/commits/$commitSha/pulls")
.appendHeader("Accept", "application/vnd.github.groot-preview+json")
.appendHeader("Authorization", "token $githubToken")
.awaitResult(GithubPullRequestDeserializer)
.appendGitHubHeaders(githubToken)
.awaitResult(GithubPullRequestListDeserializer)
.mapClientError { it.toGithubException() }

suspend fun getLatestReleaseTag(githubToken: String) =
Fuel.get("https://api.github.com/repos/flank/flank/releases/latest")
.appendHeader("Accept", "application/vnd.github.v3+json")
.appendHeader("Authorization", "token $githubToken")
.appendGitHubHeaders(githubToken)
.awaitResult(GithubReleaseDeserializable)
.mapClientError { it.toGithubException() }

Expand All @@ -37,6 +35,22 @@ fun deleteOldTag(tag: String, username: String, password: String) =
.third
.mapClientError { it.toGithubException() }

suspend fun getGitHubPullRequest(githubToken: String, issueNumber: Int) =
Fuel.get("https://api.github.com/repos/Flank/flank/pulls/$issueNumber")
.appendGitHubHeaders(githubToken)
.awaitResult(GithubPullRequestDeserializer)
.mapClientError { it.toGithubException() }

suspend fun getGitHubIssue(githubToken: String, issueNumber: Int) =
Fuel.get("https://api.github.com/repos/Flank/flank/issues/$issueNumber")
.appendGitHubHeaders(githubToken)
.awaitResult(GithubPullRequestDeserializer)
.mapClientError { it.toGithubException() }

fun Request.appendGitHubHeaders(githubToken: String) =
appendHeader("Accept", "application/vnd.github.v3+json")
.appendHeader("Authorization", "token $githubToken")

private const val DELETE_ENDPOINT = "https://api.github.com/repos/Flank/flank/git/refs/tags/"

// ============= JCABI GITHUB API =============
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package flank.scripts.github

import com.github.kittinunf.fuel.core.ResponseDeserializable
import flank.scripts.utils.toObject
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GithubPullRequest(
@SerialName("html_url") val htmlUrl: String,
val title: String,
val number: Int,
val assignees: List<GithubUser>,
val labels: List<GitHubLabel> = emptyList(),
val body: String = "",
val head: GitHubHead? = null
)

@Serializable
data class GithubUser(
val login: String,
@SerialName("html_url") val htmlUrl: String
)

object GithubPullRequestListDeserializer : ResponseDeserializable<List<GithubPullRequest>> {
override fun deserialize(content: String): List<GithubPullRequest> = content.toObject()
}

object GithubPullRequestDeserializer : ResponseDeserializable<GithubPullRequest> {
override fun deserialize(content: String): GithubPullRequest = content.toObject()
}

@Serializable
data class GitHubLabel(
val name: String
)

object GitHubLabelDeserializable : ResponseDeserializable<List<GitHubLabel>> {
override fun deserialize(content: String): List<GitHubLabel> = content.toObject()
}

@Serializable
data class GitHubHead(
val ref: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package flank.scripts.pullrequest

import flank.scripts.github.GithubPullRequest

fun GithubPullRequest.findReferenceNumber() =
(tryGetReferenceNumberFromBody() ?: tryGetReferenceNumberFromBranch())
?.trim()
?.replace("#", "")
?.toInt()

private fun GithubPullRequest.tryGetReferenceNumberFromBody() = bodyReferenceRegex.find(body)?.value

private fun GithubPullRequest.tryGetReferenceNumberFromBranch() = branchReferenceRegex.find(head?.ref.orEmpty())?.value

private val bodyReferenceRegex = "#\\d+\\s".toRegex()
private val branchReferenceRegex = "#\\d+".toRegex()
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package flank.scripts.pullrequest

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.int
import com.github.kittinunf.result.onError
import com.github.kittinunf.result.success
import flank.scripts.github.getGitHubPullRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

object CopyProperties :
CliktCommand(name = "copyProperties", help = "Copy properties from referenced issue to pull request") {

private val githubToken by option(help = "Git Token").required()
private val zenhubToken by option(help = "ZenHub api Token").required()
private val prNumber by option(help = "Pull request number").int().required()

override fun run() {
runBlocking {
getGitHubPullRequest(githubToken, prNumber)
.onError { println("Could not copy properties, because of ${it.message}") }
.success { pullRequest ->
val issueNumber = pullRequest.findReferenceNumber()
checkNotNull(issueNumber) { "Reference issue not found on description and branch" }
println("Found referenced issue #$issueNumber")
launch(Dispatchers.IO) { copyGitHubProperties(githubToken, issueNumber, prNumber) }
launch(Dispatchers.IO) { copyZenhubProperties(zenhubToken, issueNumber, prNumber) }
}
}
}
}

private suspend fun copyGitHubProperties(
githubToken: String,
baseIssueNumber: Int,
prNumber: Int
) = coroutineScope {
listOf(
launch { copyAssignees(githubToken, baseIssueNumber, prNumber) },
launch { copyLabels(githubToken, baseIssueNumber, prNumber) },
).joinAll()
}

private suspend fun copyZenhubProperties(
zenhubToken: String,
baseIssueNumber: Int,
prNumber: Int
) {
copyEstimation(zenhubToken, baseIssueNumber, prNumber)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package flank.scripts.pullrequest

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands

object PullRequestCommand : CliktCommand(name = "pullRequest") {

init {
subcommands(CopyProperties)
}

@Suppress("EmptyFunctionBlock")
override fun run() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package flank.scripts.pullrequest

import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitStringResult
import com.github.kittinunf.result.getOrNull
import com.github.kittinunf.result.map
import com.github.kittinunf.result.onError
import com.github.kittinunf.result.success
import flank.scripts.github.appendGitHubHeaders
import flank.scripts.github.getGitHubIssue
import flank.scripts.utils.toJson
import kotlinx.serialization.Serializable

suspend fun copyAssignees(githubToken: String, baseIssueNumber: Int, pullRequestNumber: Int) {
getGitHubIssue(githubToken, baseIssueNumber)
.onError { println("Could not copy assignees because of ${it.message}") }
.map { githubIssue -> githubIssue.assignees.map { it.login } }
.getOrNull()
?.let { setAssigneesToPullRequest(githubToken, pullRequestNumber, it) }
}

private suspend fun setAssigneesToPullRequest(githubToken: String, pullRequestNumber: Int, assignees: List<String>) {
Fuel.post("https://api.github.com/repos/Flank/flank/issues/$pullRequestNumber/assignees")
.appendGitHubHeaders(githubToken)
.body(SetAssigneesRequest(assignees).toJson())
.awaitStringResult()
.onError {
println("Could not set assignees because of ${it.message}")
it.printStackTrace()
}
.success { println("$assignees set to pull request #$pullRequestNumber") }
}

@Serializable
private data class SetAssigneesRequest(
val assignees: List<String>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package flank.scripts.pullrequest

import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitResult
import com.github.kittinunf.fuel.coroutines.awaitStringResult
import com.github.kittinunf.result.getOrNull
import com.github.kittinunf.result.map
import com.github.kittinunf.result.onError
import com.github.kittinunf.result.success
import flank.scripts.exceptions.mapClientError
import flank.scripts.exceptions.toGithubException
import flank.scripts.github.GitHubLabelDeserializable
import flank.scripts.github.appendGitHubHeaders
import flank.scripts.utils.toJson
import kotlinx.serialization.Serializable

suspend fun copyLabels(githubToken: String, issueNumber: Int, pullRequestNumber: Int) {
getLabelsFromIssue(githubToken, issueNumber)
.onError { println("Could not copy labels because of ${it.message}") }
.map { it.map { label -> label.name } }
.getOrNull()
?.run { setLabelsToPullRequest(githubToken, pullRequestNumber, this) }
}

private suspend fun getLabelsFromIssue(githubToken: String, issueNumber: Int) =
Fuel.get("https://api.github.com/repos/Flank/flank/issues/$issueNumber/labels")
.appendGitHubHeaders(githubToken)
.awaitResult(GitHubLabelDeserializable)
.mapClientError { it.toGithubException() }

private suspend fun setLabelsToPullRequest(githubToken: String, pullRequestNumber: Int, labels: List<String>) {
Fuel.post("https://api.github.com/repos/Flank/flank/issues/$pullRequestNumber/labels")
.appendGitHubHeaders(githubToken)
.body(SetLabelsRequest(labels).toJson())
.awaitStringResult()
.onError { println("Could not set assignees because of ${it.message}") }
.success { println("$labels set to pull request #$pullRequestNumber") }
}

@Serializable
private data class SetLabelsRequest(
val labels: List<String>
)
Loading

0 comments on commit ac5a2a8

Please sign in to comment.