Skip to content

Commit 24490ee

Browse files
committed
[#47] support run dependabot for external recipes
1 parent 423fbae commit 24490ee

10 files changed

Lines changed: 266 additions & 11 deletions

File tree

allwrite-cli/src/main/kotlin/pl/allegro/tech/allwrite/cli/application/RunWithDependabotCommand.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.serialization.Serializable
99
import org.koin.core.annotation.Single
1010
import org.openrewrite.Recipe
1111
import pl.allegro.tech.allwrite.api.RecipeExecutor
12+
import pl.allegro.tech.allwrite.api.RecipeSource
1213
import pl.allegro.tech.allwrite.cli.application.CommandExecutionResult.ExecutionResult
1314
import pl.allegro.tech.allwrite.cli.application.port.outgoing.InputFilesProvider
1415
import pl.allegro.tech.allwrite.cli.util.JSON
@@ -22,6 +23,7 @@ internal class RunWithDependabotCommand(
2223
private val inputFilesProvider: InputFilesProvider,
2324
private val recipeExecutor: RecipeExecutor,
2425
private val pullRequestDescriptionEnricher: PullRequestDescriptionEnricher,
26+
private val recipeSource: RecipeSource,
2527
) : SubCommand(
2628
name = COMMAND_NAME,
2729
help = "Finds recipe by dependabot metadata and runs it",
@@ -62,7 +64,7 @@ internal class RunWithDependabotCommand(
6264
private fun getRecipesFromDependabotMetadata(): List<String> {
6365
val dependabotMetadata = JSON.decodeFromString<PullRequestManagerExtras>(pullRequestManagerExtraParams).dependabot
6466
return dependabotMetadata
65-
.mapNotNull { it.toRecipeCoordinates() }
67+
.mapNotNull { it.toRecipeCoordinates(recipeSource.findAll()) }
6668
.flatMap { recipeMatcher.findMatching(it) }
6769
.map { it.name }
6870
.distinct()

allwrite-cli/src/main/kotlin/pl/allegro/tech/allwrite/cli/application/VersionUpdate.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package pl.allegro.tech.allwrite.cli.application
22

33
import kotlinx.serialization.Serializable
4+
import org.openrewrite.config.RecipeDescriptor
45
import pl.allegro.tech.allwrite.api.RecipeCoordinates
6+
import pl.allegro.tech.allwrite.api.tagPropertyOrNull
57
import com.github.zafarkhaja.semver.Version as DomainVersion
68

7-
private val GROUPS_BY_ARTIFACT = mapOf<String, String>()
8-
99
@Serializable
1010
internal data class PullRequestManagerExtras(
1111
val dependabot: List<VersionUpdate>,
@@ -17,9 +17,17 @@ internal data class VersionUpdate(
1717
val from: Version,
1818
val to: Version,
1919
) {
20-
21-
fun toRecipeCoordinates() =
22-
GROUPS_BY_ARTIFACT[artifact]?.let { RecipeCoordinates(it, "upgrade", DomainVersion.parse(from.normalVersion), DomainVersion.parse(to.normalVersion)) }
20+
fun toRecipeCoordinates(recipes: List<RecipeDescriptor>): RecipeCoordinates? =
21+
recipes.firstOrNull { it.tags.contains("dependabot-artifact:$artifact") }
22+
?.tagPropertyOrNull("group")
23+
?.let {
24+
RecipeCoordinates(
25+
group = it,
26+
action = "upgrade",
27+
fromVersion = DomainVersion.parse(from.normalVersion),
28+
toVersion = DomainVersion.parse(to.normalVersion),
29+
)
30+
}
2331
}
2432

2533
@Serializable

allwrite-cli/src/test/kotlin/pl/allegro/tech/allwrite/cli/ListRecipesCommandSpec.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class ListRecipesCommandSpec : BaseCliSpec() {
2323

2424
result.statusCode shouldBe 0
2525
result.output shouldBe """
26+
external-jackson/upgrade 2 3
27+
external-spring-boot/upgrade 2 3
2628
jackson/upgrade 2 3
2729
spring-boot/upgrade 2 3
2830
spring-boot/upgrade 3 4
@@ -36,6 +38,8 @@ class ListRecipesCommandSpec : BaseCliSpec() {
3638

3739
result.statusCode shouldBe 0
3840
result.output shouldBe """
41+
external-jackson/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.dependabot-jackson
42+
external-spring-boot/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.dependabot-spring-boot-3
3943
jackson/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.jackson
4044
spring-boot/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.spring-boot-3
4145
spring-boot/upgrade 3 4 -> pl.allegro.tech.allwrite.recipes.spring-boot-4
@@ -54,6 +58,8 @@ class ListRecipesCommandSpec : BaseCliSpec() {
5458

5559
result.statusCode shouldBe 0
5660
result.output shouldBe """
61+
external-jackson/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.dependabot-jackson
62+
external-spring-boot/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.dependabot-spring-boot-3
5763
jackson/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.jackson
5864
spring-boot/upgrade 2 3 -> pl.allegro.tech.allwrite.recipes.spring-boot-3
5965
spring-boot/upgrade 3 4 -> pl.allegro.tech.allwrite.recipes.spring-boot-4

allwrite-cli/src/test/kotlin/pl/allegro/tech/allwrite/cli/RunWithDependabotCommandSpec.kt

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@ package pl.allegro.tech.allwrite.cli
33
import com.github.ajalt.clikt.testing.test
44
import io.kotest.assertions.throwables.shouldThrow
55
import io.kotest.matchers.collections.shouldBeEmpty
6+
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
67
import io.kotest.matchers.equals.shouldBeEqual
78
import io.kotest.matchers.shouldBe
89
import org.koin.ksp.generated.module
910
import pl.allegro.tech.allwrite.cli.application.RunWithDependabotCommand
1011
import pl.allegro.tech.allwrite.cli.base.BaseCliSpec
1112
import pl.allegro.tech.allwrite.cli.fake.github.FakeGithubModule
12-
import pl.allegro.tech.allwrite.cli.fake.github.FakePullRequestContext
1313
import pl.allegro.tech.allwrite.runtime.fake.FakeRecipeExecutor
14+
import pl.allegro.tech.allwrite.runtime.fake.FakeRecipeSource
1415
import pl.allegro.tech.allwrite.runtime.fake.FakeRuntimeModule
1516
import pl.allegro.tech.allwrite.runtime.util.injectEagerly
1617

1718
class RunWithDependabotCommandSpec : BaseCliSpec() {
1819

1920
private val runWithDependabotCommand: RunWithDependabotCommand by injectEagerly()
2021
private val fakeRecipeExecutor: FakeRecipeExecutor by injectEagerly()
21-
private val fakePullRequestContext: FakePullRequestContext by injectEagerly()
2222

2323
override fun additionalModules() =
2424
listOf(
@@ -49,7 +49,7 @@ class RunWithDependabotCommandSpec : BaseCliSpec() {
4949
test("should finish successfully when dependabot metadata is not mapped to any recipe") {
5050
val result = runWithDependabotCommand.test(
5151
envvars = mapOf(
52-
RunWithDependabotCommand.Companion.ENV_VAR_RUN_DEPENDABOT_PAYLOAD_NAME to """
52+
RunWithDependabotCommand.ENV_VAR_RUN_DEPENDABOT_PAYLOAD_NAME to """
5353
{
5454
"dependabot": [
5555
{
@@ -67,5 +67,79 @@ class RunWithDependabotCommandSpec : BaseCliSpec() {
6767
result.output.trim() shouldBeEqual "No matching recipes found."
6868
fakeRecipeExecutor.executedRecipes.shouldBeEmpty()
6969
}
70+
71+
test("should match recipe when dependabot artifact tag is present") {
72+
val result = runWithDependabotCommand.test(
73+
envvars = mapOf(
74+
RunWithDependabotCommand.ENV_VAR_RUN_DEPENDABOT_PAYLOAD_NAME to """
75+
{
76+
"dependabot": [
77+
{
78+
"artifact":"org.springframework.boot:spring-boot-starter",
79+
"from":{"normalVersion": "2.7.0", "major": "2"},
80+
"to": {"normalVersion": "3.0.0", "major": "3"}
81+
}
82+
]
83+
}
84+
""".trimIndent(),
85+
),
86+
)
87+
88+
result.statusCode shouldBe 0
89+
fakeRecipeExecutor.executedRecipes.map { it.name } shouldContainExactlyInAnyOrder listOf(
90+
FakeRecipeSource.DEPENDABOT_SPRING_BOOT_3_TEST_RECIPE.name,
91+
)
92+
}
93+
94+
test("should not match recipe when dependabot artifact tag does not match") {
95+
val result = runWithDependabotCommand.test(
96+
envvars = mapOf(
97+
RunWithDependabotCommand.ENV_VAR_RUN_DEPENDABOT_PAYLOAD_NAME to """
98+
{
99+
"dependabot": [
100+
{
101+
"artifact":"com.example:unrelated",
102+
"from":{"normalVersion": "2.0.0", "major": "2"},
103+
"to": {"normalVersion": "3.0.0", "major": "3"}
104+
}
105+
]
106+
}
107+
""".trimIndent(),
108+
),
109+
)
110+
111+
result.statusCode shouldBe 0
112+
result.output.trim() shouldBeEqual "No matching recipes found."
113+
fakeRecipeExecutor.executedRecipes.shouldBeEmpty()
114+
}
115+
116+
test("should match multiple recipes for different artifacts in same payload") {
117+
val result = runWithDependabotCommand.test(
118+
envvars = mapOf(
119+
RunWithDependabotCommand.ENV_VAR_RUN_DEPENDABOT_PAYLOAD_NAME to """
120+
{
121+
"dependabot": [
122+
{
123+
"artifact":"org.springframework.boot:spring-boot-starter",
124+
"from":{"normalVersion": "2.7.0", "major": "2"},
125+
"to": {"normalVersion": "3.0.0", "major": "3"}
126+
},
127+
{
128+
"artifact":"com.fasterxml.jackson.core:jackson-databind",
129+
"from":{"normalVersion": "2.13.0", "major": "2"},
130+
"to": {"normalVersion": "3.0.0", "major": "3"}
131+
}
132+
]
133+
}
134+
""".trimIndent(),
135+
),
136+
)
137+
138+
result.statusCode shouldBe 0
139+
fakeRecipeExecutor.executedRecipes.map { it.name } shouldContainExactlyInAnyOrder listOf(
140+
FakeRecipeSource.DEPENDABOT_SPRING_BOOT_3_TEST_RECIPE.name,
141+
FakeRecipeSource.DEPENDABOT_JACKSON_TEST_RECIPE.name,
142+
)
143+
}
70144
}
71145
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package pl.allegro.tech.allwrite.cli
2+
3+
import com.github.zafarkhaja.semver.Version
4+
import io.kotest.core.spec.style.FunSpec
5+
import io.kotest.matchers.shouldBe
6+
import org.openrewrite.config.RecipeDescriptor
7+
import pl.allegro.tech.allwrite.api.RecipeCoordinates
8+
import pl.allegro.tech.allwrite.cli.application.VersionUpdate
9+
import java.net.URI
10+
import pl.allegro.tech.allwrite.cli.application.Version as VersionDto
11+
12+
class VersionUpdateSpec : FunSpec() {
13+
init {
14+
test("should resolve coordinates when matching dependabot-artifact tag exists") {
15+
val update = VersionUpdate("org.example:lib", VersionDto("2.7.0"), VersionDto("3.0.0"))
16+
val recipes = listOf(
17+
RecipeDescriptor(setOf("group:my-group", "action:upgrade", "dependabot-artifact:org.example:lib")),
18+
)
19+
20+
update.toRecipeCoordinates(recipes) shouldBe RecipeCoordinates(
21+
group = "my-group",
22+
action = "upgrade",
23+
fromVersion = Version.parse("2.7.0"),
24+
toVersion = Version.parse("3.0.0"),
25+
)
26+
}
27+
28+
test("should return null when no recipe has matching dependabot-artifact tag") {
29+
val update = VersionUpdate("org.example:lib", VersionDto("1.0.0"), VersionDto("2.0.0"))
30+
val recipes = listOf(
31+
RecipeDescriptor(setOf("group:other", "action:upgrade", "dependabot-artifact:org.example:other-lib")),
32+
)
33+
34+
update.toRecipeCoordinates(recipes) shouldBe null
35+
}
36+
37+
test("should return null when recipe list is empty") {
38+
val update = VersionUpdate("org.example:lib", VersionDto("1.0.0"), VersionDto("2.0.0"))
39+
40+
update.toRecipeCoordinates(emptyList()) shouldBe null
41+
}
42+
43+
test("should match first recipe when multiple recipes have matching tag") {
44+
val update = VersionUpdate("org.example:lib", VersionDto("1.0.0"), VersionDto("2.0.0"))
45+
val recipes = listOf(
46+
RecipeDescriptor(setOf("group:first-group", "action:upgrade", "dependabot-artifact:org.example:lib")),
47+
RecipeDescriptor(setOf("group:second-group", "action:upgrade", "dependabot-artifact:org.example:lib")),
48+
)
49+
50+
update.toRecipeCoordinates(recipes)?.group shouldBe "first-group"
51+
}
52+
}
53+
}
54+
55+
@Suppress("FunctionNaming")
56+
private fun RecipeDescriptor(tags: Set<String>) =
57+
RecipeDescriptor(
58+
"name",
59+
"display name",
60+
"instance name",
61+
"description",
62+
tags,
63+
null,
64+
emptyList(),
65+
emptyList(),
66+
emptyList(),
67+
emptyList(),
68+
emptyList(),
69+
emptyList(),
70+
URI("file:///not-used"),
71+
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package pl.allegro.tech.allwrite.runtime
2+
3+
import io.kotest.core.spec.style.FunSpec
4+
import io.kotest.matchers.collections.shouldContain
5+
import io.kotest.matchers.collections.shouldContainAll
6+
import io.kotest.matchers.shouldBe
7+
import pl.allegro.tech.allwrite.RecipeMetadata
8+
import pl.allegro.tech.allwrite.RecipeVisibility
9+
10+
class RecipeMetadataSpec : FunSpec() {
11+
init {
12+
test("should produce dependabot-artifact tag when dependabotArtifacts is provided") {
13+
val metadata = RecipeMetadata(
14+
displayName = "Test",
15+
description = "Test.",
16+
visibility = RecipeVisibility.PUBLIC,
17+
group = "test-group",
18+
action = "upgrade",
19+
from = "1",
20+
to = "2",
21+
dependabotArtifacts = listOf("org.example:lib"),
22+
)
23+
24+
metadata.tags shouldContain "dependabot-artifact:org.example:lib"
25+
}
26+
27+
test("should produce no dependabot-artifact tags when dependabotArtifacts is empty") {
28+
val metadata = RecipeMetadata(
29+
displayName = "Test",
30+
description = "Test.",
31+
visibility = RecipeVisibility.PUBLIC,
32+
group = "test-group",
33+
action = "upgrade",
34+
from = "1",
35+
to = "2",
36+
dependabotArtifacts = emptyList(),
37+
)
38+
39+
metadata.tags.none { it.startsWith("dependabot-artifact:") } shouldBe true
40+
}
41+
42+
test("should produce multiple dependabot-artifact tags for multiple artifacts") {
43+
val metadata = RecipeMetadata(
44+
displayName = "Test",
45+
description = "Test.",
46+
visibility = RecipeVisibility.PUBLIC,
47+
group = "test-group",
48+
action = "upgrade",
49+
from = "1",
50+
to = "2",
51+
dependabotArtifacts = listOf("org.example:lib-a", "org.example:lib-b"),
52+
)
53+
54+
metadata.tags shouldContainAll listOf(
55+
"dependabot-artifact:org.example:lib-a",
56+
"dependabot-artifact:org.example:lib-b",
57+
)
58+
}
59+
}
60+
}

allwrite-runtime/src/testFixtures/kotlin/pl/allegro/tech/allwrite/runtime/fake/FakeRecipeSource.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,32 @@ class FakeRecipeSource(
6868
description = "Some Picnic rule",
6969
tags = emptySet(),
7070
)
71+
val DEPENDABOT_SPRING_BOOT_3_TEST_RECIPE = FakeRecipe(
72+
id = "pl.allegro.tech.allwrite.recipes.dependabot-spring-boot-3",
73+
displayName = "Dependabot Spring Boot 2 to 3",
74+
description = "Dependabot-triggered Spring Boot upgrade.",
75+
tags = setOf(
76+
"visibility:PUBLIC",
77+
"from:2",
78+
"to:3",
79+
"group:external-spring-boot",
80+
"action:upgrade",
81+
"dependabot-artifact:org.springframework.boot:spring-boot-starter",
82+
),
83+
)
84+
val DEPENDABOT_JACKSON_TEST_RECIPE = FakeRecipe(
85+
id = "pl.allegro.tech.allwrite.recipes.dependabot-jackson",
86+
displayName = "Dependabot Jackson upgrade",
87+
description = "Dependabot-triggered Jackson upgrade.",
88+
tags = setOf(
89+
"visibility:PUBLIC",
90+
"from:2",
91+
"to:3",
92+
"group:external-jackson",
93+
"action:upgrade",
94+
"dependabot-artifact:com.fasterxml.jackson.core:jackson-databind",
95+
),
96+
)
7197
val TEST_RECIPES = listOf(
7298
SPRING_BOOT_3_TEST_RECIPE,
7399
SPRING_BOOT_4_TEST_RECIPE,
@@ -76,6 +102,8 @@ class FakeRecipeSource(
76102
EXPAND_MAPPINGS_TEST_RECIPE,
77103
OPENREWRITE_TEST_RECIPE,
78104
PICNIC_TEST_RECIPE,
105+
DEPENDABOT_SPRING_BOOT_3_TEST_RECIPE,
106+
DEPENDABOT_JACKSON_TEST_RECIPE,
79107
)
80108
}
81109
}

allwrite-spi/src/main/kotlin/pl/allegro/tech/allwrite/AllwriteRecipe.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ public abstract class AllwriteRecipe(
1010
public val action: String? = null,
1111
public val from: String? = null,
1212
public val to: String? = null,
13+
public val dependabotArtifacts: List<String> = emptyList(),
1314
) : Recipe() {
1415

15-
public val metadata: RecipeMetadata = RecipeMetadata(displayName, description, visibility, group, action, from, to)
16+
public val metadata: RecipeMetadata =
17+
RecipeMetadata(displayName, description, visibility, group, action, from, to, dependabotArtifacts)
1618

1719
override fun getDisplayName(): String = metadata.displayName
1820
override fun getDescription(): String = metadata.description

0 commit comments

Comments
 (0)