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

ci: Implement workflow for full suite IT (cron + manual) #1353

Merged
merged 14 commits into from
Dec 14, 2020
62 changes: 62 additions & 0 deletions .github/workflows/full_suite_integration_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Full Suite Integration Tests

on:
schedule:
- cron: '0 0 * * 1-5' # At 00:00 on every day-of-week from Monday through Friday
workflow_dispatch: # or manually

jobs:
run-it-full-suite:
if: github.ref == 'refs/heads/master' # for now we want this workflow to run only on master, to be removed once support for different branches is implemented
runs-on: ubuntu-latest
outputs:
build-scan-url: ${{ steps.run-it.outputs.build-scan-url }}
it-status: ${{ steps.status.outputs.it-status }}
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: '${{ secrets.GITHUB_TOKEN }}'

- name: Checkout code
uses: actions/checkout@v2
with:
submodules: true

- name: Download flankScripts and add it to PATH
run: |
./gradlew :flank-scripts:download
echo "./flank-scripts/bash" >> $GITHUB_PATH

- uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ubuntu-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
ubuntu-gradle-

- name: Gradle clean build
uses: eskatos/gradle-command-action@v1
id: build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_REF: ${{ github.ref }}
with:
arguments: "clean assemble"

- name: Gradle integration tests
uses: eskatos/gradle-command-action@v1
id: run-it
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_REF: ${{ github.ref }}
with:
arguments: "integrationTests"

- name: Process IT results
run: |
flankScripts integration processResults \
--result ${{ job.status }} \
--url ${{ steps.run-it.outputs.build-scan-url }} \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--run-id ${{ github.run_id }}
19 changes: 19 additions & 0 deletions flank-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,22 @@ or
| --github-token | GitHub Token |
| --zenhub-token | ZenHub api Token |
| --pr-number | Pull request number |

### integration (currently, available only on the master branch)
To show all available commands for `integration` use:
`flankScripts integration`

Available commands are: Process results from `full_suite_integration_tests` workflow
- `processResults`

#### `processResults`
If IT ended up with failure a new issue is created with some basic info: build scan URL, commit list, timestamp.
If there is an issue already created comment is posted to the existing one (so we avoid multiple copies of the same ticket).
Once integration tests end with success issue is closed.

| Option | Description |
|----------------|---------------------|
| --github-token | GitHub Token |
| --result | Status of IT step from workflow. Can be either `success` or `failure`|
| --url | Build scan url from IT step|
| --run-id | `job.run_id` value from workflow context. Used for comment posting|
2 changes: 1 addition & 1 deletion flank-scripts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ shadowJar.apply {
}
}
// <breaking change>.<feature added>.<fix/minor change>
version = "1.1.5"
version = "1.2.0"
group = "com.github.flank"

application {
Expand Down
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.integration.IntegrationCommand
import flank.scripts.pullrequest.PullRequestCommand
import flank.scripts.release.ReleaseCommand
import flank.scripts.shell.ShellCommand
Expand All @@ -21,6 +22,7 @@ fun main(args: Array<String>) {
DependenciesCommand,
TestArtifactsCommand(),
ShellCommand,
PullRequestCommand
PullRequestCommand,
IntegrationCommand
).main(args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package flank.scripts.integration

import com.github.kittinunf.result.getOrElse
import com.github.kittinunf.result.onError
import flank.scripts.github.getGitHubCommitList
import flank.scripts.github.getPrDetailsByCommit
import flank.scripts.github.objects.GithubPullRequest
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope

suspend fun getCommitListSinceDate(
token: String,
since: String
): List<Pair<String, GithubPullRequest?>> = coroutineScope {
getGitHubCommitList(token, listOf("since" to since))
.onError { println(it.message) }
.getOrElse { emptyList() }
.map {
async {
it.sha to getPrDetailsByCommit(it.sha, token).getOrElse { emptyList() }
}
}
pawelpasterz marked this conversation as resolved.
Show resolved Hide resolved
.awaitAll()
.flatMap { (commit, prs) ->
if (prs.isEmpty()) listOf(commit to null)
else prs.map { commit to it }
}
pawelpasterz marked this conversation as resolved.
Show resolved Hide resolved
.toList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package flank.scripts.integration

import com.github.kittinunf.result.onError
import flank.scripts.github.objects.GitHubCreateIssueCommentRequest
import flank.scripts.github.objects.GitHubCreateIssueRequest
import flank.scripts.github.objects.GitHubCreateIssueResponse
import flank.scripts.github.objects.GitHubUpdateIssueRequest
import flank.scripts.github.objects.IssueState
import flank.scripts.github.patchIssue
import flank.scripts.github.postNewIssue
import flank.scripts.github.postNewIssueComment
import flank.scripts.utils.toJson
import kotlinx.coroutines.coroutineScope
import kotlin.system.exitProcess

internal suspend fun IntegrationContext.createNewIssue(): GitHubCreateIssueCommentRequest =
createAndPostNewIssue().postComment()

internal suspend fun IntegrationContext.postComment(): GitHubCreateIssueCommentRequest =
createCommentPayload().also { payload ->
postNewIssueComment(token, issueNumber, payload)
println("** Comment posted")
println(payload.toJson())
}

internal suspend fun IntegrationContext.closeIssue(): ByteArray =
postComment().run {
println("** Closing issue")
patchIssue(token, issueNumber, GitHubUpdateIssueRequest(state = IssueState.CLOSED)).get()
}

private suspend fun IntegrationContext.createAndPostNewIssue(payload: GitHubCreateIssueRequest = createIssuePayload()) =
postNewIssue(token, payload)
.onError {
println(it.message)
exitProcess(-1)
}
.get()
.run {
logIssueCreated(this)
copy(openedIssue = number)
}

private fun createIssuePayload(): GitHubCreateIssueRequest {
println("** Creating new issue")
val issuePayload = GitHubCreateIssueRequest(
title = "Full Suite integration tests failed on master",
body = "### Integration Test failed on master",
labels = listOf("IT_Failed", "bug")
)
println(issuePayload.toJson())
return issuePayload
}

private suspend fun IntegrationContext.createCommentPayload() = coroutineScope {
val message = when (result) {
ITResults.SUCCESS -> prepareSuccessMessage(lastRun, runID, url)
ITResults.FAILURE -> {
val commitList = getCommitListSinceDate(token, lastRun)
prepareFailureMessage(lastRun, runID, url, commitList)
}
}
GitHubCreateIssueCommentRequest(message)
}

private fun logIssueCreated(issue: GitHubCreateIssueResponse) = println(
"""
** Issue created:
url: ${issue.htmlUrl}
number: ${issue.number}
""".trimIndent()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package flank.scripts.integration

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

object IntegrationCommand : CliktCommand(name = "integration") {

init {
subcommands(ProcessResultCommand)
}

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

data class IntegrationContext(
val result: ITResults,
val token: String,
val url: String,
val runID: String,
val lastRun: String,
private val openedIssue: Int?,
) {
val issueNumber: Int
get() = requireNotNull(openedIssue)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package flank.scripts.integration

import com.github.kittinunf.result.getOrElse
import com.github.kittinunf.result.onError
import flank.scripts.github.getGitHubIssueList

suspend fun checkForOpenedITIssues(token: String): Int? = getGitHubIssueList(
githubToken = token,
parameters = listOf(
"creator" to "github-actions[bot]",
"state" to "open",
"labels" to "IT_Failed"
)
)
.onError { println(it.message) }
.getOrElse { emptyList() }
.firstOrNull()
.also {
if (it != null) println("** Issue found: ${it.htmlUrl}")
else println("** No opened issue")
}?.number
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package flank.scripts.integration

import flank.scripts.github.objects.GithubPullRequest
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

fun prepareSuccessMessage(
lastRun: String,
runId: String,
url: String
): String = successTemplate(lastRun, runId, url)

fun prepareFailureMessage(
lastRun: String,
runId: String,
url: String,
prs: List<Pair<String, GithubPullRequest?>>
): String = buildString {
appendLine(failureTemplate(lastRun, runId, url))
if (prs.isEmpty()) appendLine("No new commits")
else {
appendLine("|commit SHA|PR|")
appendLine("|---|:---:|")
prs.forEach { (commit, pr) ->
appendLine("|$commit|${pr?.let { "[${it.title}](${it.htmlUrl})" } ?: "---"}")
}
}
}

private val successTemplate = { lastRun: String, runId: String, url: String ->
pawelpasterz marked this conversation as resolved.
Show resolved Hide resolved
"""
|### Full suite IT run :white_check_mark: SUCCEEDED :white_check_mark:
|**Timestamp:** ${makeHumanFriendly(lastRun)}
|**Job run:** [$runId](https://github.com/Flank/flank/actions/runs/$runId
|**Build scan URL:** $url
|**Closing issue**
""".trimMargin()
}

private val failureTemplate = { lastRun: String, runId: String, url: String ->
"""
|### Full suite IT run :x: FAILED :x:
|**Timestamp:** ${makeHumanFriendly(lastRun)}
|**Job run:** [$runId](https://github.com/Flank/flank/actions/runs/$runId
|**Build scan URL:** $url
""".trimMargin()
}

private fun makeHumanFriendly(date: String) =
LocalDateTime
.ofInstant(Instant.from(DateTimeFormatter.ISO_INSTANT.parse(date)), ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")

package flank.scripts.integration

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.enum
import kotlinx.coroutines.runBlocking

object ProcessResultCommand : CliktCommand(name = "processResults") {

private val itResult by option(help = "IT run job status", names = arrayOf("--result"))
.enum<ITResults>(ignoreCase = true)
.required()

private val buildScanURL by option(help = "Gradle build scan URL", names = arrayOf("--url"))
.required()

private val githubToken by option(help = "Git Token").required()
private val runID by option(help = "Workflow job ID").required()
private val openedIssue by lazy { runBlocking { checkForOpenedITIssues(githubToken) } }
private val lastRun by lazy { runBlocking { getLastITWorkflowRunDate(githubToken) } }

override fun run() {
runBlocking {
logArgs()
with(makeContext()) {
when {
itResult == ITResults.FAILURE && openedIssue == null -> createNewIssue()
itResult == ITResults.FAILURE && openedIssue != null -> postComment()
itResult == ITResults.SUCCESS && openedIssue != null -> closeIssue()
else -> return@runBlocking
}
}
}
}

private fun makeContext() = IntegrationContext(
result = itResult,
token = githubToken,
url = buildScanURL,
runID = runID,
lastRun = lastRun,
openedIssue = openedIssue
)

private fun logArgs() = println(
"""
** Parameters:
result: $itResult
url: $buildScanURL
runID: $runID
""".trimIndent()
)
}

enum class ITResults {
SUCCESS, FAILURE
}
Loading