Skip to content

Commit d387213

Browse files
committed
Initial version of bumpdeps
1 parent 1cb7927 commit d387213

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-0
lines changed

Dockerfile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM openjdk:11
2+
COPY . /bumpdeps
3+
RUN /bumpdeps/gradlew --no-daemon -p /bumpdeps installDist
4+
ENTRYPOINT ["/bumpdeps/build/install/bumpdeps/bin/bumpdeps"]

action.yml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: 'Bump Spinnaker Dependencies'
2+
description: 'Bump dependencies in external repositories upon a release'
3+
inputs:
4+
ref:
5+
description: 'the release ref triggering this dependency bump'
6+
required: true
7+
key:
8+
description: 'the key in gradle.properties to modify'
9+
required: true
10+
repositories:
11+
description: 'the comma-separated list of repository names to modify'
12+
required: true
13+
repoOwner:
14+
description: 'the owner of the repositories to modify'
15+
required: true
16+
default: spinnakerbot
17+
upstreamOwner:
18+
description: 'the owner of the repositories to send pull requests to'
19+
required: true
20+
default: spinnaker
21+
reviewers:
22+
description: "the comma-separated list of reviewers (prefixed with 'team:' for a team) for the pull request"
23+
required: true
24+
default: 'team:oss-reviewers'
25+
runs:
26+
using: 'docker'
27+
image: 'Dockerfile'
28+
args:
29+
- '--ref'
30+
- ${{ inputs.ref }}
31+
- '--key'
32+
- ${{ inputs.key }}
33+
- '--repositories'
34+
- ${{ inputs.repositories }}
35+
- '--repo-owner'
36+
- ${{ inputs.repoOwner }}
37+
- '--upstream-owner'
38+
- ${{ inputs.upstreamOwner }}
39+
- '--reviewers'
40+
- ${{ inputs.reviewers }}

build.gradle.kts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
val kotlinVersion = "1.3.71"
3+
kotlin("jvm") version kotlinVersion
4+
id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
5+
application
6+
}
7+
8+
repositories {
9+
mavenCentral()
10+
jcenter()
11+
}
12+
13+
dependencies {
14+
implementation("com.github.ajalt:clikt:2.5.0")
15+
implementation("io.github.microutils:kotlin-logging:1.7.8")
16+
implementation("org.eclipse.jgit:org.eclipse.jgit:5.7.0.202003110725-r")
17+
implementation("org.kohsuke:github-api:1.109")
18+
implementation("nu.studer:java-ordered-properties:1.0.2")
19+
20+
runtimeOnly("org.slf4j:slf4j-simple:1.7.30")
21+
}
22+
23+
application {
24+
mainClassName = "io.spinnaker.bumpdeps.MainKt"
25+
}
26+
27+
ktlint {
28+
enableExperimentalRules.set(true)
29+
}
30+
31+
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
32+
kotlinOptions.jvmTarget = "11"
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package io.spinnaker.bumpdeps
2+
3+
import com.github.ajalt.clikt.core.CliktCommand
4+
import com.github.ajalt.clikt.core.UsageError
5+
import com.github.ajalt.clikt.parameters.options.convert
6+
import com.github.ajalt.clikt.parameters.options.default
7+
import com.github.ajalt.clikt.parameters.options.option
8+
import com.github.ajalt.clikt.parameters.options.required
9+
import com.github.ajalt.clikt.parameters.options.split
10+
import java.io.FileInputStream
11+
import java.io.FileOutputStream
12+
import java.nio.file.Files
13+
import java.nio.file.Path
14+
import java.util.concurrent.ExecutionException
15+
import java.util.concurrent.Executors
16+
import java.util.concurrent.Future
17+
import java.util.concurrent.TimeUnit
18+
import kotlin.system.exitProcess
19+
import mu.KotlinLogging
20+
import nu.studer.java.util.OrderedProperties.OrderedPropertiesBuilder
21+
import org.eclipse.jgit.api.Git
22+
import org.eclipse.jgit.transport.RefSpec
23+
import org.eclipse.jgit.transport.URIish
24+
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
25+
import org.kohsuke.github.GHIssueState
26+
import org.kohsuke.github.GitHubBuilder
27+
28+
class BumpDeps : CliktCommand() {
29+
30+
private val logger = KotlinLogging.logger {}
31+
32+
companion object {
33+
val REF_PREFIX = "refs/tags/v"
34+
const val GITHUB_OAUTH_TOKEN_ENV_NAME = "GITHUB_OAUTH"
35+
}
36+
37+
private val version by option("--ref", help = "the release ref triggering this dependency bump").convert { ref ->
38+
if (!ref.startsWith(REF_PREFIX)) {
39+
fail("Ref '$ref' is not a valid release ref")
40+
}
41+
ref.removePrefix(REF_PREFIX)
42+
}.required()
43+
44+
private val key by option(help = "the key in gradle.properties to modify")
45+
.required()
46+
47+
private val repositories by option(help = "the comma-separated list of repository names to modify")
48+
.split(",")
49+
.required()
50+
51+
private val repoOwner by option(help = "the owner of the repositories to modify")
52+
.required()
53+
54+
private val upstreamOwner by option(help = "the owner of the repositories to send pull requests to")
55+
.required()
56+
57+
private val reviewers by option(help = "the comma-separated list of reviewers (prefixed with 'team:' for a team) for the pull request")
58+
.convert { convertReviewersArg(it) }
59+
.default(Reviewers())
60+
61+
private val oauthToken by lazy {
62+
val oauthToken = System.getenv(GITHUB_OAUTH_TOKEN_ENV_NAME)
63+
if (oauthToken == null) {
64+
throw UsageError("A GitHub OAuth token must be provided in the $GITHUB_OAUTH_TOKEN_ENV_NAME environment variable")
65+
}
66+
oauthToken
67+
}
68+
69+
override fun run() {
70+
val repoParent = createTempDirectory()
71+
72+
val executor = Executors.newCachedThreadPool()
73+
val results = mutableMapOf<String, Future<*>>()
74+
repositories.forEach { repoName ->
75+
results[repoName] = executor.submit {
76+
val branchName = "autobump-$key"
77+
createModifiedBranch(repoParent, repoName, branchName)
78+
createPullRequest(repoName, branchName)
79+
}
80+
}
81+
82+
executor.shutdown()
83+
executor.awaitTermination(10, TimeUnit.MINUTES)
84+
85+
val failures = waitForResults(results)
86+
87+
if (failures) {
88+
exitProcess(1)
89+
}
90+
}
91+
92+
private fun createModifiedBranch(
93+
repoParent: Path,
94+
repoName: String,
95+
branchName: String
96+
) {
97+
val credentialsProvider =
98+
UsernamePasswordCredentialsProvider("ignored-username", oauthToken)
99+
100+
val repoRoot = repoParent.resolve(repoName)
101+
val upstreamUri = "https://github.com/$upstreamOwner/$repoName"
102+
logger.info { "Cloning $upstreamUri to $repoRoot" }
103+
val git = Git.cloneRepository()
104+
.setCredentialsProvider(credentialsProvider)
105+
.setURI(upstreamUri)
106+
.setDirectory(repoRoot.toFile())
107+
.call()
108+
val gradlePropsFile = repoRoot.resolve("gradle.properties")
109+
updatePropertiesFile(gradlePropsFile, repoName)
110+
if (git.branchList().call().map { it.name }.contains(branchName)) {
111+
git.branchDelete().setBranchNames(branchName).setForce(true).call()
112+
}
113+
git.checkout().setName(branchName).setCreateBranch(true).call()
114+
git.commit().setMessage("chore(dependencies): Autobump $key").setAll(true).call()
115+
val userUri = "https://github.com/$repoOwner/$repoName"
116+
git.remoteAdd().setName("userFork").setUri(URIish(userUri)).call()
117+
logger.info { "Force-pushing changes to $userUri" }
118+
git.push()
119+
.setCredentialsProvider(credentialsProvider)
120+
.setRemote("userFork")
121+
.setRefSpecs(RefSpec("$branchName:$branchName"))
122+
.setForce(true)
123+
.call()
124+
}
125+
126+
private fun updatePropertiesFile(gradlePropsFile: Path, repoName: String) {
127+
val gradleProps = FileInputStream(gradlePropsFile.toFile()).use { fis ->
128+
OrderedPropertiesBuilder()
129+
.withOrdering(String.CASE_INSENSITIVE_ORDER)
130+
.withSuppressDateInComment(true)
131+
.build()
132+
.apply { load(fis) }
133+
}
134+
if (!gradleProps.containsProperty(key)) {
135+
throw IllegalArgumentException("Couldn't locate key $key in $repoName's gradle.properties file")
136+
}
137+
if (gradleProps.getProperty(key) == version) {
138+
throw IllegalArgumentException("$repoName's $key is already set to $version")
139+
}
140+
gradleProps.setProperty(key, version)
141+
FileOutputStream(gradlePropsFile.toFile()).use { fos ->
142+
gradleProps.store(fos, /* comments= */ null)
143+
}
144+
}
145+
146+
private fun createPullRequest(repoName: String, branchName: String) {
147+
val github = GitHubBuilder().withOAuthToken(oauthToken).build()
148+
val githubRepo = github.getRepository("$upstreamOwner/$repoName")
149+
150+
// If there's already an existing PR, we can just reuse it... we already force-pushed the branch, so it'll
151+
// automatically update.
152+
val existingPr = githubRepo.getPullRequests(GHIssueState.OPEN)
153+
.firstOrNull { pr -> pr.labels.map { label -> label.name }.contains(branchName) }
154+
155+
if (existingPr != null) {
156+
logger.info { "Found existing PR for repo $repoName: ${existingPr.htmlUrl}" }
157+
return
158+
}
159+
160+
logger.info { "Creating pull request for repo $repoName" }
161+
val pr = githubRepo.createPullRequest(
162+
/* title= */"chore(dependencies): Autobump $key",
163+
/* head= */ "$repoOwner:$branchName",
164+
/* base= */ "master",
165+
/* body= */ ""
166+
)
167+
168+
pr.addLabels(branchName)
169+
170+
if (reviewers.users.isNotEmpty()) {
171+
pr.requestReviewers(reviewers.users.map { github.getUser(it) })
172+
}
173+
if (reviewers.teams.isNotEmpty()) {
174+
val upstreamOrg = github.getOrganization(upstreamOwner)
175+
pr.requestTeamReviewers(reviewers.teams.map { upstreamOrg.getTeamByName(it) })
176+
}
177+
178+
logger.info { "Created pull request for $repoName: ${pr.htmlUrl}" }
179+
}
180+
181+
data class Reviewers(val users: Set<String> = setOf(), val teams: Set<String> = setOf())
182+
private fun convertReviewersArg(reviewersString: String): Reviewers {
183+
val reviewers = reviewersString.split(',').map { it.trim() }.filter { it.isNotEmpty() }.toSet()
184+
val teams = reviewers.filter { it.startsWith("team:") }.toSet()
185+
val users = reviewers - teams
186+
return Reviewers(users, teams.map { it.removePrefix("team:") }.toSet())
187+
}
188+
189+
private fun waitForResults(results: MutableMap<String, Future<*>>): Boolean {
190+
var failures = false
191+
results
192+
.forEach { result ->
193+
try {
194+
result.value.get()
195+
} catch (e: ExecutionException) {
196+
logger.error(e) { "Exception updating repository ${result.key}" }
197+
failures = true
198+
}
199+
}
200+
return failures
201+
}
202+
203+
private fun createTempDirectory(): Path {
204+
val repoParent = Files.createTempDirectory("bumpdeps-git-")
205+
Runtime.getRuntime().addShutdownHook(object : Thread() {
206+
override fun run() {
207+
repoParent.toFile().deleteRecursively()
208+
}
209+
})
210+
return repoParent
211+
}
212+
}
213+
214+
fun main(args: Array<String>) {
215+
BumpDeps().main(args)
216+
}

0 commit comments

Comments
 (0)