Skip to content

Commit 14eca8a

Browse files
authored
fix: display full response and don't fail if GitHub responds with non-success (#1858)
Part of #1855. This PR also adds graceful handling of non-success responses from GitHub: instead of exceptions we started using Arrow Kt and `Either`.
1 parent 35d78ce commit 14eca8a

File tree

7 files changed

+93
-58
lines changed

7 files changed

+93
-58
lines changed

action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.typesafegithub.workflows.updates
22

3+
import arrow.core.getOrElse
34
import io.github.typesafegithub.workflows.domain.ActionStep
45
import io.github.typesafegithub.workflows.domain.Workflow
56
import io.github.typesafegithub.workflows.domain.actions.RegularAction
@@ -54,7 +55,9 @@ internal suspend fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubToken:
5455
owner = actionOwner,
5556
name = actionName.substringBefore('/'),
5657
githubToken = githubToken,
57-
)
58+
).getOrElse {
59+
throw Exception(it)
60+
}
5861
} catch (e: Exception) {
5962
githubError(
6063
"failed to fetch versions for $actionOwner/$actionName, skipping",

maven-binding-builder/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ plugins {
44

55
dependencies {
66
implementation("org.jetbrains.kotlin:kotlin-compiler")
7+
api("io.arrow-kt:arrow-core:2.0.1")
78
api(projects.actionBindingGenerator)
89
implementation(projects.sharedInternal)
10+
implementation("io.github.oshai:kotlin-logging:7.0.5")
911

1012
runtimeOnly(projects.githubWorkflowsKt)
1113
}

maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
package io.github.typesafegithub.workflows.mavenbinding
22

3+
import arrow.core.Either
4+
import arrow.core.getOrElse
5+
import io.github.oshai.kotlinlogging.KotlinLogging.logger
36
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords
47
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL
58
import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions
69
import io.github.typesafegithub.workflows.shared.internal.model.Version
710
import java.time.format.DateTimeFormatter
811

12+
private val logger = logger { }
13+
914
internal suspend fun ActionCoords.buildMavenMetadataFile(
1015
githubToken: String,
11-
fetchAvailableVersions: suspend (owner: String, name: String, githubToken: String?) -> List<Version> = ::fetchAvailableVersions,
16+
fetchAvailableVersions: suspend (
17+
owner: String,
18+
name: String,
19+
githubToken: String?,
20+
) -> Either<String, List<Version>> = ::fetchAvailableVersions,
1221
): String? {
1322
val availableVersions =
1423
fetchAvailableVersions(owner, name, githubToken)
15-
.filter { it.isMajorVersion() || (significantVersion < FULL) }
24+
.getOrElse {
25+
logger.error { it }
26+
emptyList()
27+
}.filter { it.isMajorVersion() || (significantVersion < FULL) }
1628
val newest = availableVersions.maxOrNull() ?: return null
1729
val lastUpdated =
1830
DateTimeFormatter

maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.typesafegithub.workflows.mavenbinding
22

3+
import arrow.core.Either
4+
import arrow.core.right
35
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords
46
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion
57
import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL
@@ -20,7 +22,7 @@ class MavenMetadataBuildingTest :
2022

2123
test("various kinds of versions available") {
2224
// Given
23-
val fetchAvailableVersions: suspend (String, String, String?) -> List<Version> = { _, _, _ ->
25+
val fetchAvailableVersions: suspend (String, String, String?) -> Either<String, List<Version>> = { _, _, _ ->
2426
listOf(
2527
Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }),
2628
Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }),
@@ -30,7 +32,7 @@ class MavenMetadataBuildingTest :
3032
Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }),
3133
Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }),
3234
Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }),
33-
)
35+
).right()
3436
}
3537

3638
val xml =
@@ -60,14 +62,14 @@ class MavenMetadataBuildingTest :
6062

6163
test("no major versions") {
6264
// Given
63-
val fetchAvailableVersions: suspend (String, String, String?) -> List<Version> = { _, _, _ ->
65+
val fetchAvailableVersions: suspend (String, String, String?) -> Either<String, List<Version>> = { _, _, _ ->
6466
listOf(
6567
Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }),
6668
Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }),
6769
Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }),
6870
Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }),
6971
Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }),
70-
)
72+
).right()
7173
}
7274

7375
val xml =
@@ -81,8 +83,8 @@ class MavenMetadataBuildingTest :
8183

8284
test("no versions available") {
8385
// Given
84-
val fetchAvailableVersions: suspend (String, String, String?) -> List<Version> = { _, _, _ ->
85-
emptyList()
86+
val fetchAvailableVersions: suspend (String, String, String?) -> Either<String, List<Version>> = { _, _, _ ->
87+
emptyList<Version>().right()
8688
}
8789

8890
val xml =
@@ -97,7 +99,7 @@ class MavenMetadataBuildingTest :
9799
(SignificantVersion.entries - FULL).forEach { significantVersion ->
98100
test("significant version $significantVersion requested") {
99101
// Given
100-
val fetchAvailableVersions: suspend (String, String, String?) -> List<Version> = { owner, name, _ ->
102+
val fetchAvailableVersions: suspend (String, String, String?) -> Either<String, List<Version>> = { owner, name, _ ->
101103
listOf(
102104
Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }),
103105
Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }),
@@ -107,7 +109,7 @@ class MavenMetadataBuildingTest :
107109
Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }),
108110
Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }),
109111
Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }),
110-
)
112+
).right()
111113
}
112114

113115
val xml =

shared-internal/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ group = rootProject.group
99
version = rootProject.version
1010

1111
dependencies {
12+
api("io.arrow-kt:arrow-core:2.0.1")
1213
// we cannot use a BOM due to limitation in kotlin scripting when resolving the transitive KMM variant dependencies
1314
// note: see https://youtrack.jetbrains.com/issue/KT-67618
1415
api("io.ktor:ktor-client-core:3.1.1")

shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.github.typesafegithub.workflows.shared.internal
22

3+
import arrow.core.Either
4+
import arrow.core.raise.either
5+
import arrow.core.raise.ensure
36
import io.github.typesafegithub.workflows.shared.internal.model.Version
47
import io.ktor.client.HttpClient
58
import io.ktor.client.call.body
@@ -8,6 +11,8 @@ import io.ktor.client.engine.cio.CIO
811
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
912
import io.ktor.client.request.bearerAuth
1013
import io.ktor.client.request.get
14+
import io.ktor.client.statement.bodyAsText
15+
import io.ktor.http.isSuccess
1116
import io.ktor.serialization.kotlinx.json.json
1217
import kotlinx.serialization.Serializable
1318
import kotlinx.serialization.json.Json
@@ -18,50 +23,60 @@ suspend fun fetchAvailableVersions(
1823
name: String,
1924
githubToken: String?,
2025
httpClientEngine: HttpClientEngine = CIO.create(),
21-
): List<Version> {
22-
val httpClient = buildHttpClient(engine = httpClientEngine)
23-
return listOf(
24-
apiTagsUrl(owner = owner, name = name),
25-
apiBranchesUrl(owner = owner, name = name),
26-
).flatMap { url -> fetchGithubRefs(url, githubToken, httpClient) }
27-
.versions(githubToken, httpClient)
28-
}
26+
): Either<String, List<Version>> =
27+
either {
28+
val httpClient = buildHttpClient(engine = httpClientEngine)
29+
return listOf(
30+
apiTagsUrl(owner = owner, name = name),
31+
apiBranchesUrl(owner = owner, name = name),
32+
).flatMap { url -> fetchGithubRefs(url, githubToken, httpClient).bind() }
33+
.versions(githubToken, httpClient)
34+
}
2935

3036
private fun List<GithubRef>.versions(
3137
githubToken: String?,
3238
httpClient: HttpClient,
33-
): List<Version> =
34-
this.map { githubRef ->
35-
val version = githubRef.ref.substringAfterLast("/")
36-
Version(version) {
37-
val response =
38-
httpClient
39-
.get(urlString = githubRef.`object`.url) {
40-
if (githubToken != null) {
41-
bearerAuth(githubToken)
39+
): Either<String, List<Version>> =
40+
either {
41+
this@versions.map { githubRef ->
42+
val version = githubRef.ref.substringAfterLast("/")
43+
Version(version) {
44+
val response =
45+
httpClient
46+
.get(urlString = githubRef.`object`.url) {
47+
if (githubToken != null) {
48+
bearerAuth(githubToken)
49+
}
4250
}
43-
}
44-
val releaseDate =
45-
when (githubRef.`object`.type) {
46-
"tag" -> response.body<Tag>().tagger
47-
"commit" -> response.body<Commit>().author
48-
else -> error("Unexpected target object type ${githubRef.`object`.type}")
49-
}.date
50-
ZonedDateTime.parse(releaseDate)
51+
val releaseDate =
52+
when (githubRef.`object`.type) {
53+
"tag" -> response.body<Tag>().tagger
54+
"commit" -> response.body<Commit>().author
55+
else -> error("Unexpected target object type ${githubRef.`object`.type}")
56+
}.date
57+
ZonedDateTime.parse(releaseDate)
58+
}
5159
}
5260
}
5361

5462
private suspend fun fetchGithubRefs(
5563
url: String,
5664
githubToken: String?,
5765
httpClient: HttpClient,
58-
): List<GithubRef> =
59-
httpClient
60-
.get(urlString = url) {
61-
if (githubToken != null) {
62-
bearerAuth(githubToken)
63-
}
64-
}.body()
66+
): Either<String, List<GithubRef>> =
67+
either {
68+
val response =
69+
httpClient
70+
.get(urlString = url) {
71+
if (githubToken != null) {
72+
bearerAuth(githubToken)
73+
}
74+
}
75+
ensure(response.status.isSuccess()) {
76+
"Unexpected response when fetching refs from $url. Status: ${response.status}, response: ${response.bodyAsText()}"
77+
}
78+
response.body()
79+
}
6580

6681
private fun apiTagsUrl(
6782
owner: String,

shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package io.github.typesafegithub.workflows.shared.internal.model
22

3+
import arrow.core.left
4+
import arrow.core.right
35
import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions
4-
import io.kotest.assertions.throwables.shouldThrow
56
import io.kotest.core.spec.style.FunSpec
67
import io.kotest.matchers.shouldBe
7-
import io.kotest.matchers.string.shouldContain
88
import io.ktor.client.engine.mock.MockEngine
99
import io.ktor.client.engine.mock.respond
1010
import io.ktor.http.HttpHeaders
1111
import io.ktor.http.HttpStatusCode
1212
import io.ktor.http.fullPath
1313
import io.ktor.http.headersOf
14-
import io.ktor.serialization.JsonConvertException
1514
import io.ktor.utils.io.ByteReadChannel
1615

1716
class GithubApiTest :
@@ -95,7 +94,7 @@ class GithubApiTest :
9594
}
9695

9796
// When
98-
val versions =
97+
val versionsOrError =
9998
fetchAvailableVersions(
10099
owner = "some-owner",
101100
name = "some-name",
@@ -104,13 +103,13 @@ class GithubApiTest :
104103
)
105104

106105
// Then
107-
versions shouldBe
106+
versionsOrError shouldBe
108107
listOf(
109108
Version("v1.0.0"),
110109
Version("v1.0.1"),
111110
Version("v1"),
112111
Version("v2"),
113-
)
112+
).right()
114113
}
115114

116115
test("error occurs when fetching branches and tags") {
@@ -125,20 +124,21 @@ class GithubApiTest :
125124
)
126125
}
127126

128-
// Then
129-
// TODO: fix - right now, the logic fails if it gets something unparseable.
130-
// The test just shows the current behavior, not the intended behavior.
131-
// To be fixed in https://github.com/typesafegithub/github-workflows-kt/issues/1855
132-
shouldThrow<JsonConvertException> {
133-
// When
127+
// When
128+
val versionsOrError =
134129
fetchAvailableVersions(
135130
owner = "some-owner",
136131
name = "some-name",
137132
githubToken = "token",
138133
httpClientEngine = mockEngine,
139134
)
140-
}.also {
141-
it.message shouldContain "Unexpected JSON token"
142-
}
135+
136+
// Then
137+
versionsOrError shouldBe
138+
(
139+
"Unexpected response when fetching refs from " +
140+
"https://api.github.com/repos/some-owner/some-name/git/matching-refs/tags/v. " +
141+
"Status: 403 Forbidden, response: {\"message\": \"There was a problem!\"}"
142+
).left()
143143
}
144144
})

0 commit comments

Comments
 (0)