From 4a918ee8a5e0d80830b61b3fd94c19ad31930edd Mon Sep 17 00:00:00 2001 From: Leonardo Colman Lopes Date: Fri, 25 Apr 2025 20:08:08 -0300 Subject: [PATCH 1/4] refactor(server): simplify GitHub API usage - Replace manual HTTP client implementation with `kohsuke.github` library. - Simplify `fetchAvailableVersions` logic and remove redundant dependencies. - Refactor tests to use `kotest` with `MockServer` for more concise and structured testing. - Update dependencies and remove unused code. --- .../typesafegithub/workflows/updates/Utils.kt | 7 +- .../mavenbinding/MavenMetadataBuilding.kt | 12 +- .../mavenbinding/MavenMetadataBuildingTest.kt | 72 +++--- shared-internal/build.gradle.kts | 3 +- .../workflows/shared/internal/GithubApi.kt | 153 ++---------- .../shared/internal/model/GithubApiTest.kt | 234 +++++++----------- shared-internal/src/test/resources/heads.json | 22 ++ .../src/test/resources/repository.json | 110 ++++++++ shared-internal/src/test/resources/tags.json | 22 ++ 9 files changed, 312 insertions(+), 323 deletions(-) create mode 100644 shared-internal/src/test/resources/heads.json create mode 100644 shared-internal/src/test/resources/repository.json create mode 100644 shared-internal/src/test/resources/tags.json diff --git a/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt b/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt index 623835f8d..38d78c82e 100644 --- a/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt +++ b/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt @@ -1,6 +1,5 @@ package io.github.typesafegithub.workflows.updates -import arrow.core.getOrElse import io.github.typesafegithub.workflows.domain.ActionStep import io.github.typesafegithub.workflows.domain.Workflow import io.github.typesafegithub.workflows.domain.actions.RegularAction @@ -28,7 +27,7 @@ internal suspend fun Workflow.availableVersionsForEachAction( groupedSteps.forEach { (action, steps) -> val availableVersions = action.fetchAvailableVersionsOrWarn( - githubAuthToken = githubAuthToken, + githubAuthToken = githubAuthToken ?: error("github auth token is required"), ) val currentVersion = Version(action.actionVersion) if (availableVersions != null) { @@ -49,7 +48,7 @@ internal suspend fun Workflow.availableVersionsForEachAction( } } -internal suspend fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubAuthToken: String?): List? = +internal fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubAuthToken: String): List? = try { fetchAvailableVersions( owner = actionOwner, @@ -58,7 +57,7 @@ internal suspend fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubAuthTok ).getOrElse { throw Exception(it) } - } catch (e: Exception) { + } catch (_: Exception) { githubError( "failed to fetch versions for $actionOwner/$actionName, skipping", ) diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt index ef1fe1499..d549ff68c 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt @@ -1,23 +1,23 @@ package io.github.typesafegithub.workflows.mavenbinding -import arrow.core.Either import arrow.core.getOrElse import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL -import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions import io.github.typesafegithub.workflows.shared.internal.model.Version +import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions as defaultFetchAvailableVersions private val logger = logger { } internal suspend fun ActionCoords.buildMavenMetadataFile( githubAuthToken: String, - fetchAvailableVersions: suspend ( + fetchAvailableVersions: ( owner: String, name: String, - githubAuthToken: String?, - ) -> Either> = ::fetchAvailableVersions, + githubAuthToken: String, + ) -> Result> = ::defaultFetchAvailableVersions, prefetchBindingArtifacts: (Collection) -> Unit = {}, ): String? { val availableVersions = @@ -31,7 +31,7 @@ internal suspend fun ActionCoords.buildMavenMetadataFile( val lastUpdated = DateTimeFormatter .ofPattern("yyyyMMddHHmmss") - .format(newest.getReleaseDate()) + .format(newest.getReleaseDate() ?: ZonedDateTime.now()) return """ diff --git a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt index f8b56c005..07009d9c6 100644 --- a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt +++ b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt @@ -1,7 +1,5 @@ package io.github.typesafegithub.workflows.mavenbinding -import arrow.core.Either -import arrow.core.right import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL @@ -22,17 +20,19 @@ class MavenMetadataBuildingTest : test("various kinds of versions available") { // Given - val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { _, _, _ -> - listOf( - Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), - Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), - Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), - Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - ).right() + val fetchAvailableVersions: (String, String, String?) -> Result> = { _, _, _ -> + Result.success( + listOf( + Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), + Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), + Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), + Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + ), + ) } val xml = @@ -62,14 +62,16 @@ class MavenMetadataBuildingTest : test("no major versions") { // Given - val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { _, _, _ -> - listOf( - Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), - Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - ).right() + val fetchAvailableVersions: (String, String, String?) -> Result> = { _, _, _ -> + Result.success( + listOf( + Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), + Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + ), + ) } val xml = @@ -83,8 +85,8 @@ class MavenMetadataBuildingTest : test("no versions available") { // Given - val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { _, _, _ -> - emptyList().right() + val fetchAvailableVersions: (String, String, String?) -> Result> = { _, _, _ -> + Result.success(emptyList()) } val xml = @@ -99,17 +101,19 @@ class MavenMetadataBuildingTest : (SignificantVersion.entries - FULL).forEach { significantVersion -> test("significant version $significantVersion requested") { // Given - val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { owner, name, _ -> - listOf( - Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), - Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), - Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), - Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - ).right() + val fetchAvailableVersions: (String, String, String?) -> Result> = { owner, name, _ -> + Result.success( + listOf( + Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), + Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), + Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), + Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + ), + ) } val xml = diff --git a/shared-internal/build.gradle.kts b/shared-internal/build.gradle.kts index 71f760ab7..6b9f1be7f 100644 --- a/shared-internal/build.gradle.kts +++ b/shared-internal/build.gradle.kts @@ -31,5 +31,6 @@ dependencies { // Here's a ticket to remember to remove this workaround: https://github.com/typesafegithub/github-workflows-kt/issues/1832 runtimeOnly("org.jetbrains.kotlinx:kotlinx-io-core:0.7.0") - testImplementation("io.ktor:ktor-client-mock:3.1.3") + testImplementation("io.kotest.extensions:kotest-extensions-mockserver:1.3.0") + testImplementation("org.slf4j:slf4j-simple:2.0.12") } diff --git a/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt b/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt index cbdfb0985..76d125319 100644 --- a/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt +++ b/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt @@ -1,147 +1,22 @@ package io.github.typesafegithub.workflows.shared.internal -import arrow.core.Either -import arrow.core.raise.either -import arrow.core.raise.ensure -import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.github.typesafegithub.workflows.shared.internal.model.Version -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.HttpClientEngineFactory -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel.ALL -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.request.bearerAuth -import io.ktor.client.request.get -import io.ktor.client.statement.bodyAsText -import io.ktor.http.isSuccess -import io.ktor.serialization.kotlinx.json.json -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import java.time.ZonedDateTime +import org.kohsuke.github.GHRef +import org.kohsuke.github.GitHubBuilder -private val logger = logger { } - -suspend fun fetchAvailableVersions( +fun fetchAvailableVersions( owner: String, name: String, - githubAuthToken: String?, - httpClientEngineFactory: HttpClientEngineFactory<*> = CIO, -): Either> = - either { - buildHttpClient(engineFactory = httpClientEngineFactory).use { httpClient -> - return listOf( - apiTagsUrl(owner = owner, name = name), - apiBranchesUrl(owner = owner, name = name), - ).flatMap { url -> fetchGithubRefs(url, githubAuthToken, httpClient).bind() } - .versions(githubAuthToken, httpClientEngineFactory) - } - } - -private fun List.versions( - githubAuthToken: String?, - httpClientEngineFactory: HttpClientEngineFactory<*>, -): Either> = - either { - this@versions.map { githubRef -> - val version = githubRef.ref.substringAfterLast("/") - Version(version) { - val response = - buildHttpClient(engineFactory = httpClientEngineFactory).use { httpClient -> - httpClient - .get(urlString = githubRef.`object`.url) { - if (githubAuthToken != null) { - bearerAuth(githubAuthToken) - } - } - } - val releaseDate = - when (githubRef.`object`.type) { - "tag" -> response.body().tagger - "commit" -> response.body().author - else -> error("Unexpected target object type ${githubRef.`object`.type}") - }.date - ZonedDateTime.parse(releaseDate) - } - } - } - -private suspend fun fetchGithubRefs( - url: String, - githubAuthToken: String?, - httpClient: HttpClient, -): Either> = - either { - val response = - httpClient - .get(urlString = url) { - if (githubAuthToken != null) { - bearerAuth(githubAuthToken) - } - } - ensure(response.status.isSuccess()) { - "Unexpected response when fetching refs from $url. " + - "Status: ${response.status}, response: ${response.bodyAsText()}" - } - response.body() + githubAuthToken: String, + githubEndpoint: String = "https://api.github.com", +): Result> = + runCatching { + val github = GitHubBuilder().withEndpoint(githubEndpoint).withOAuthToken(githubAuthToken).build() + val repository = github.getRepository("$owner/$name") + val apiTags = repository.getRefs("tags").refsStartingWithV().map { Version(it) } + val apiHeads = repository.getRefs("heads").refsStartingWithV().map { Version(it) } + + apiTags + apiHeads } -private fun apiTagsUrl( - owner: String, - name: String, -): String = "https://api.github.com/repos/$owner/$name/git/matching-refs/tags/v" - -private fun apiBranchesUrl( - owner: String, - name: String, -): String = "https://api.github.com/repos/$owner/$name/git/matching-refs/heads/v" - -@Serializable -private data class GithubRef( - val ref: String, - val `object`: Object, -) - -@Serializable -private data class Object( - val type: String, - val url: String, -) - -@Serializable -private data class Tag( - val tagger: Person, -) - -@Serializable -private data class Commit( - val author: Person, -) - -@Serializable -private data class Person( - val date: String, -) - -private fun buildHttpClient(engineFactory: HttpClientEngineFactory<*>) = - HttpClient(engineFactory) { - val klogger = logger - install(Logging) { - logger = - object : Logger { - override fun log(message: String) { - klogger.trace { message } - } - } - level = ALL - } - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - }, - ) - } - } +private fun Array.refsStartingWithV() = map { it.ref.substringAfterLast('/') }.filter { it.startsWith("v") } diff --git a/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt b/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt index 5ceab7be0..604acb6df 100644 --- a/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt +++ b/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt @@ -1,150 +1,106 @@ package io.github.typesafegithub.workflows.shared.internal.model -import arrow.core.left -import arrow.core.right import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions +import io.kotest.core.extensions.install import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.ktor.client.engine.HttpClientEngineFactory -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.MockEngineConfig -import io.ktor.client.engine.mock.respond -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.fullPath -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteReadChannel +import io.kotest.extensions.mockserver.MockServerExtension +import io.kotest.matchers.result.shouldBeFailure +import io.kotest.matchers.result.shouldBeSuccess +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest.request +import org.mockserver.model.HttpResponse.response class GithubApiTest : - FunSpec({ - test("branches with major versions and tags with other versions") { - // Given - val tagsResponse = - """ - [ - { - "ref":"refs/tags/v1.0.0", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1", - "object": { - "sha":"544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9", - "type":"tag", - "url":"https://api.github.com/repos/actions/some-name/git/tags/544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9" - } - }, - { - "ref":"refs/tags/v1.0.1", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1.0.1", - "object": { - "sha":"af513c7a016048ae468971c52ed77d9562c7c819", - "type":"tag", - "url":"https://api.github.com/repos/actions/some-name/git/tags/af513c7a016048ae468971c52ed77d9562c7c819" - } - } - ] - """.trimIndent() - val headsResponse = - """ - [ - { - "ref":"refs/heads/v1", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvc2lsZW50LXJldi1wYXJzZQ==", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v1", - "object": { - "sha":"af5130cb8882054eda385840657dcbd1e19ab8f4", - "type":"commit", - "url":"https://api.github.com/repos/some-owner/some-name/git/commits/af5130cb8882054eda385840657dcbd1e19ab8f4" - } - }, - { - "ref":"refs/heads/v2", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvdG9vbGtpdC13aW5kb3dzLWV4ZWM=", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v2", - "object": { - "sha":"c22ccee38a13e34cb01a103c324adb1db665821e", - "type":"commit", - "url":"https://api.github.com/repos/some-owner/some-name/git/commits/c22ccee38a13e34cb01a103c324adb1db665821e" - } - } - ] - """.trimIndent() - val mockEngineFactory = - object : HttpClientEngineFactory { - override fun create(block: MockEngineConfig.() -> Unit) = - MockEngine { request -> - if ("matching-refs/tags" in request.url.fullPath) { - respond( - // language=json - content = ByteReadChannel(tagsResponse), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json"), - ) - } else if ("matching-refs/heads" in request.url.fullPath) { - respond( - // language=json - content = ByteReadChannel(headsResponse), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json"), - ) - } else { - respond( - content = ByteReadChannel("The mock client wasn't prepared for this request"), - status = HttpStatusCode.NotFound, - ) - } - } - } + FunSpec( + { + val mockServer = install(MockServerExtension()) - // When - val versionsOrError = - fetchAvailableVersions( - owner = "some-owner", - name = "some-name", - githubAuthToken = "token", - httpClientEngineFactory = mockEngineFactory, - ) + beforeTest { + mockServer.reset() + } - // Then - versionsOrError shouldBe - listOf( - Version("v1.0.0"), - Version("v1.0.1"), - Version("v1"), - Version("v2"), - ).right() - } + val owner = "some-owner" + val name = "some-name" - test("error occurs when fetching branches and tags") { - // Given - val mockEngineFactory = - object : HttpClientEngineFactory { - override fun create(block: MockEngineConfig.() -> Unit) = - MockEngine { request -> - respond( - // language=json - content = ByteReadChannel("""{"message": "There was a problem!"}"""), - status = HttpStatusCode.Forbidden, - headers = headersOf(HttpHeaders.ContentType, "application/json"), - ) - } - } + test("branches with major versions and tags with other versions") { + // Given + mockServer.mockRepositoryResponse(owner, name) + mockServer.mockTagsResponse(owner, name) + mockServer.mockHeadsResponse(owner, name) - // When - val versionsOrError = - fetchAvailableVersions( - owner = "some-owner", - name = "some-name", - githubAuthToken = "token", - httpClientEngineFactory = mockEngineFactory, - ) + // When + val versionsOrError = + fetchAvailableVersions( + owner = owner, + name = name, + githubAuthToken = "token", + githubEndpoint = "http://localhost:${mockServer.port}", + ) - // Then - versionsOrError shouldBe - ( - "Unexpected response when fetching refs from " + - "https://api.github.com/repos/some-owner/some-name/git/matching-refs/tags/v. " + - "Status: 403 Forbidden, response: {\"message\": \"There was a problem!\"}" - ).left() - } - }) + // Then + versionsOrError shouldBeSuccess + listOf( + Version("v1.0.0"), + Version("v1.0.1"), + Version("v1"), + Version("v2"), + ) + } + + test("error occurs when fetching branches and tags") { + // Given + // No mocks setup (will fail) + + // When + val versionOrError = + fetchAvailableVersions( + owner = owner, + name = name, + githubAuthToken = "token", + githubEndpoint = "http://localhost:${mockServer.port}", + ) + + // Then + versionOrError.shouldBeFailure() + } + }, + ) + +private fun ClientAndServer.mockHeadsResponse( + owner: String, + name: String, +) { + mockResponse("/repos/$owner/$name/git/refs/heads", "heads.json") +} + +private fun ClientAndServer.mockTagsResponse( + owner: String, + name: String, +) { + mockResponse("/repos/$owner/$name/git/refs/tags", "tags.json") +} + +private fun ClientAndServer.mockRepositoryResponse( + owner: String, + name: String, +) { + mockResponse("/repos/$owner/$name", "repository.json") +} + +private fun ClientAndServer.mockResponse( + path: String, + resource: String, +) { + `when`(request().withPath(path)) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody(readResource(resource)), + ) +} + +fun readResource(path: String) = + GithubApiTest::class.java.classLoader + .getResourceAsStream(path)!! + .readBytes() diff --git a/shared-internal/src/test/resources/heads.json b/shared-internal/src/test/resources/heads.json new file mode 100644 index 000000000..82630a78f --- /dev/null +++ b/shared-internal/src/test/resources/heads.json @@ -0,0 +1,22 @@ +[ + { + "ref":"refs/heads/v1", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvc2lsZW50LXJldi1wYXJzZQ==", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v1", + "object": { + "sha":"af5130cb8882054eda385840657dcbd1e19ab8f4", + "type":"commit", + "url":"https://api.github.com/repos/some-owner/some-name/git/commits/af5130cb8882054eda385840657dcbd1e19ab8f4" + } + }, + { + "ref":"refs/heads/v2", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvdG9vbGtpdC13aW5kb3dzLWV4ZWM=", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v2", + "object": { + "sha":"c22ccee38a13e34cb01a103c324adb1db665821e", + "type":"commit", + "url":"https://api.github.com/repos/some-owner/some-name/git/commits/c22ccee38a13e34cb01a103c324adb1db665821e" + } + } +] \ No newline at end of file diff --git a/shared-internal/src/test/resources/repository.json b/shared-internal/src/test/resources/repository.json new file mode 100644 index 000000000..bc3c1ea57 --- /dev/null +++ b/shared-internal/src/test/resources/repository.json @@ -0,0 +1,110 @@ +{ + "id": 429460367, + "node_id": "R_kgDOGZkLjw", + "name": "some-name", + "full_name": "some-owner/some-name", + "private": false, + "owner": { + "login": "some-owner", + "id": 1577251, + "node_id": "MDQ6VXNlcjE1NzcyNTE=", + "avatar_url": "https://avatars.githubusercontent.com/u/1577251?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/LeoColman", + "html_url": "https://github.com/LeoColman", + "followers_url": "https://api.github.com/users/LeoColman/followers", + "following_url": "https://api.github.com/users/LeoColman/following{/other_user}", + "gists_url": "https://api.github.com/users/LeoColman/gists{/gist_id}", + "starred_url": "https://api.github.com/users/LeoColman/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/LeoColman/subscriptions", + "organizations_url": "https://api.github.com/users/LeoColman/orgs", + "repos_url": "https://api.github.com/users/LeoColman/repos", + "events_url": "https://api.github.com/users/LeoColman/events{/privacy}", + "received_events_url": "https://api.github.com/users/LeoColman/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/LeoColman/MyStack", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/LeoColman/MyStack", + "forks_url": "https://api.github.com/repos/LeoColman/MyStack/forks", + "keys_url": "https://api.github.com/repos/LeoColman/MyStack/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/LeoColman/MyStack/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/LeoColman/MyStack/teams", + "hooks_url": "https://api.github.com/repos/LeoColman/MyStack/hooks", + "issue_events_url": "https://api.github.com/repos/LeoColman/MyStack/issues/events{/number}", + "events_url": "https://api.github.com/repos/LeoColman/MyStack/events", + "assignees_url": "https://api.github.com/repos/LeoColman/MyStack/assignees{/user}", + "branches_url": "https://api.github.com/repos/LeoColman/MyStack/branches{/branch}", + "tags_url": "https://api.github.com/repos/LeoColman/MyStack/tags", + "blobs_url": "https://api.github.com/repos/LeoColman/MyStack/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/LeoColman/MyStack/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/LeoColman/MyStack/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/LeoColman/MyStack/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/LeoColman/MyStack/statuses/{sha}", + "languages_url": "https://api.github.com/repos/LeoColman/MyStack/languages", + "stargazers_url": "https://api.github.com/repos/LeoColman/MyStack/stargazers", + "contributors_url": "https://api.github.com/repos/LeoColman/MyStack/contributors", + "subscribers_url": "https://api.github.com/repos/LeoColman/MyStack/subscribers", + "subscription_url": "https://api.github.com/repos/LeoColman/MyStack/subscription", + "commits_url": "https://api.github.com/repos/LeoColman/MyStack/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/LeoColman/MyStack/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/LeoColman/MyStack/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/LeoColman/MyStack/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/LeoColman/MyStack/contents/{+path}", + "compare_url": "https://api.github.com/repos/LeoColman/MyStack/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/LeoColman/MyStack/merges", + "archive_url": "https://api.github.com/repos/LeoColman/MyStack/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/LeoColman/MyStack/downloads", + "issues_url": "https://api.github.com/repos/LeoColman/MyStack/issues{/number}", + "pulls_url": "https://api.github.com/repos/LeoColman/MyStack/pulls{/number}", + "milestones_url": "https://api.github.com/repos/LeoColman/MyStack/milestones{/number}", + "notifications_url": "https://api.github.com/repos/LeoColman/MyStack/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/LeoColman/MyStack/labels{/name}", + "releases_url": "https://api.github.com/repos/LeoColman/MyStack/releases{/id}", + "deployments_url": "https://api.github.com/repos/LeoColman/MyStack/deployments", + "created_at": "2021-11-18T14:26:50Z", + "updated_at": "2025-04-23T19:38:18Z", + "pushed_at": "2025-04-23T19:38:15Z", + "git_url": "git://github.com/LeoColman/MyStack.git", + "ssh_url": "git@github.com:LeoColman/MyStack.git", + "clone_url": "https://github.com/LeoColman/MyStack.git", + "svn_url": "https://github.com/LeoColman/MyStack", + "homepage": null, + "size": 24074, + "stargazers_count": 1, + "watchers_count": 1, + "language": null, + "has_issues": true, + "has_projects": false, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 1, + "watchers": 1, + "default_branch": "main", + "temp_clone_token": null, + "network_count": 1, + "subscribers_count": 1 +} \ No newline at end of file diff --git a/shared-internal/src/test/resources/tags.json b/shared-internal/src/test/resources/tags.json new file mode 100644 index 000000000..d0eaa71e9 --- /dev/null +++ b/shared-internal/src/test/resources/tags.json @@ -0,0 +1,22 @@ +[ + { + "ref":"refs/tags/v1.0.0", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1", + "object": { + "sha":"544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9", + "type":"tag", + "url":"https://api.github.com/repos/actions/some-name/git/tags/544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9" + } + }, + { + "ref":"refs/tags/v1.0.1", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1.0.1", + "object": { + "sha":"af513c7a016048ae468971c52ed77d9562c7c819", + "type":"tag", + "url":"https://api.github.com/repos/actions/some-name/git/tags/af513c7a016048ae468971c52ed77d9562c7c819" + } + } +] \ No newline at end of file From 4fb842d240c7842a7dfd6bd591020c374f91ffef Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Sat, 17 May 2025 22:11:12 +0200 Subject: [PATCH 2/4] Partially sync with new tests --- .../typesafegithub/workflows/updates/Utils.kt | 7 +- .../workflows/shared/internal/GithubApi.kt | 19 +- .../shared/internal/model/GithubApiTest.kt | 256 ++++++++++++++---- 3 files changed, 226 insertions(+), 56 deletions(-) diff --git a/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt b/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt index 38d78c82e..623835f8d 100644 --- a/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt +++ b/action-updates-checker/src/main/kotlin/io/github/typesafegithub/workflows/updates/Utils.kt @@ -1,5 +1,6 @@ package io.github.typesafegithub.workflows.updates +import arrow.core.getOrElse import io.github.typesafegithub.workflows.domain.ActionStep import io.github.typesafegithub.workflows.domain.Workflow import io.github.typesafegithub.workflows.domain.actions.RegularAction @@ -27,7 +28,7 @@ internal suspend fun Workflow.availableVersionsForEachAction( groupedSteps.forEach { (action, steps) -> val availableVersions = action.fetchAvailableVersionsOrWarn( - githubAuthToken = githubAuthToken ?: error("github auth token is required"), + githubAuthToken = githubAuthToken, ) val currentVersion = Version(action.actionVersion) if (availableVersions != null) { @@ -48,7 +49,7 @@ internal suspend fun Workflow.availableVersionsForEachAction( } } -internal fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubAuthToken: String): List? = +internal suspend fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubAuthToken: String?): List? = try { fetchAvailableVersions( owner = actionOwner, @@ -57,7 +58,7 @@ internal fun RegularAction<*>.fetchAvailableVersionsOrWarn(githubAuthToken: Stri ).getOrElse { throw Exception(it) } - } catch (_: Exception) { + } catch (e: Exception) { githubError( "failed to fetch versions for $actionOwner/$actionName, skipping", ) diff --git a/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt b/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt index 76d125319..630c7d737 100644 --- a/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt +++ b/shared-internal/src/main/kotlin/io/github/typesafegithub/workflows/shared/internal/GithubApi.kt @@ -1,17 +1,28 @@ package io.github.typesafegithub.workflows.shared.internal +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.right import io.github.typesafegithub.workflows.shared.internal.model.Version import org.kohsuke.github.GHRef +import org.kohsuke.github.GHRepository import org.kohsuke.github.GitHubBuilder fun fetchAvailableVersions( owner: String, name: String, - githubAuthToken: String, + githubAuthToken: String?, githubEndpoint: String = "https://api.github.com", -): Result> = - runCatching { - val github = GitHubBuilder().withEndpoint(githubEndpoint).withOAuthToken(githubAuthToken).build() +): Either> = + either { + val github = + GitHubBuilder() + .withEndpoint(githubEndpoint) + .also { + if (githubAuthToken != null) { + it.withOAuthToken(githubAuthToken) + } + }.build() val repository = github.getRepository("$owner/$name") val apiTags = repository.getRefs("tags").refsStartingWithV().map { Version(it) } val apiHeads = repository.getRefs("heads").refsStartingWithV().map { Version(it) } diff --git a/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt b/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt index 604acb6df..fdd953397 100644 --- a/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt +++ b/shared-internal/src/test/kotlin/io/github/typesafegithub/workflows/shared/internal/model/GithubApiTest.kt @@ -1,12 +1,12 @@ package io.github.typesafegithub.workflows.shared.internal.model +import arrow.core.left +import arrow.core.right import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions import io.kotest.core.extensions.install import io.kotest.core.spec.style.FunSpec import io.kotest.extensions.mockserver.MockServerExtension -import io.kotest.matchers.result.shouldBeFailure -import io.kotest.matchers.result.shouldBeSuccess -import org.mockserver.integration.ClientAndServer +import io.kotest.matchers.shouldBe import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response @@ -24,9 +24,193 @@ class GithubApiTest : test("branches with major versions and tags with other versions") { // Given - mockServer.mockRepositoryResponse(owner, name) - mockServer.mockTagsResponse(owner, name) - mockServer.mockHeadsResponse(owner, name) + val repositoryResponse = + """ + { + "id": 429460367, + "node_id": "R_kgDOGZkLjw", + "name": "some-name", + "full_name": "some-owner/some-name", + "private": false, + "owner": { + "login": "some-owner", + "id": 1577251, + "node_id": "MDQ6VXNlcjE1NzcyNTE=", + "avatar_url": "https://avatars.githubusercontent.com/u/1577251?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/LeoColman", + "html_url": "https://github.com/LeoColman", + "followers_url": "https://api.github.com/users/LeoColman/followers", + "following_url": "https://api.github.com/users/LeoColman/following{/other_user}", + "gists_url": "https://api.github.com/users/LeoColman/gists{/gist_id}", + "starred_url": "https://api.github.com/users/LeoColman/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/LeoColman/subscriptions", + "organizations_url": "https://api.github.com/users/LeoColman/orgs", + "repos_url": "https://api.github.com/users/LeoColman/repos", + "events_url": "https://api.github.com/users/LeoColman/events{/privacy}", + "received_events_url": "https://api.github.com/users/LeoColman/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "html_url": "https://github.com/LeoColman/MyStack", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/LeoColman/MyStack", + "forks_url": "https://api.github.com/repos/LeoColman/MyStack/forks", + "keys_url": "https://api.github.com/repos/LeoColman/MyStack/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/LeoColman/MyStack/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/LeoColman/MyStack/teams", + "hooks_url": "https://api.github.com/repos/LeoColman/MyStack/hooks", + "issue_events_url": "https://api.github.com/repos/LeoColman/MyStack/issues/events{/number}", + "events_url": "https://api.github.com/repos/LeoColman/MyStack/events", + "assignees_url": "https://api.github.com/repos/LeoColman/MyStack/assignees{/user}", + "branches_url": "https://api.github.com/repos/LeoColman/MyStack/branches{/branch}", + "tags_url": "https://api.github.com/repos/LeoColman/MyStack/tags", + "blobs_url": "https://api.github.com/repos/LeoColman/MyStack/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/LeoColman/MyStack/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/LeoColman/MyStack/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/LeoColman/MyStack/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/LeoColman/MyStack/statuses/{sha}", + "languages_url": "https://api.github.com/repos/LeoColman/MyStack/languages", + "stargazers_url": "https://api.github.com/repos/LeoColman/MyStack/stargazers", + "contributors_url": "https://api.github.com/repos/LeoColman/MyStack/contributors", + "subscribers_url": "https://api.github.com/repos/LeoColman/MyStack/subscribers", + "subscription_url": "https://api.github.com/repos/LeoColman/MyStack/subscription", + "commits_url": "https://api.github.com/repos/LeoColman/MyStack/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/LeoColman/MyStack/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/LeoColman/MyStack/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/LeoColman/MyStack/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/LeoColman/MyStack/contents/{+path}", + "compare_url": "https://api.github.com/repos/LeoColman/MyStack/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/LeoColman/MyStack/merges", + "archive_url": "https://api.github.com/repos/LeoColman/MyStack/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/LeoColman/MyStack/downloads", + "issues_url": "https://api.github.com/repos/LeoColman/MyStack/issues{/number}", + "pulls_url": "https://api.github.com/repos/LeoColman/MyStack/pulls{/number}", + "milestones_url": "https://api.github.com/repos/LeoColman/MyStack/milestones{/number}", + "notifications_url": "https://api.github.com/repos/LeoColman/MyStack/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/LeoColman/MyStack/labels{/name}", + "releases_url": "https://api.github.com/repos/LeoColman/MyStack/releases{/id}", + "deployments_url": "https://api.github.com/repos/LeoColman/MyStack/deployments", + "created_at": "2021-11-18T14:26:50Z", + "updated_at": "2025-04-23T19:38:18Z", + "pushed_at": "2025-04-23T19:38:15Z", + "git_url": "git://github.com/LeoColman/MyStack.git", + "ssh_url": "git@github.com:LeoColman/MyStack.git", + "clone_url": "https://github.com/LeoColman/MyStack.git", + "svn_url": "https://github.com/LeoColman/MyStack", + "homepage": null, + "size": 24074, + "stargazers_count": 1, + "watchers_count": 1, + "language": null, + "has_issues": true, + "has_projects": false, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "has_discussions": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 1, + "watchers": 1, + "default_branch": "main", + "temp_clone_token": null, + "network_count": 1, + "subscribers_count": 1 + } + """.trimIndent() + val tagsResponse = + """ + [ + { + "ref":"refs/tags/v1.0.0", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1", + "object": { + "sha":"544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9", + "type":"tag", + "url":"https://api.github.com/repos/actions/some-name/git/tags/544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9" + } + }, + { + "ref":"refs/tags/v1.0.1", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1.0.1", + "object": { + "sha":"af513c7a016048ae468971c52ed77d9562c7c819", + "type":"tag", + "url":"https://api.github.com/repos/actions/some-name/git/tags/af513c7a016048ae468971c52ed77d9562c7c819" + } + } + ] + """.trimIndent() + val headsResponse = + """ + [ + { + "ref":"refs/heads/v1", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvc2lsZW50LXJldi1wYXJzZQ==", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v1", + "object": { + "sha":"af5130cb8882054eda385840657dcbd1e19ab8f4", + "type":"commit", + "url":"https://api.github.com/repos/some-owner/some-name/git/commits/af5130cb8882054eda385840657dcbd1e19ab8f4" + } + }, + { + "ref":"refs/heads/v2", + "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvdG9vbGtpdC13aW5kb3dzLWV4ZWM=", + "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v2", + "object": { + "sha":"c22ccee38a13e34cb01a103c324adb1db665821e", + "type":"commit", + "url":"https://api.github.com/repos/some-owner/some-name/git/commits/c22ccee38a13e34cb01a103c324adb1db665821e" + } + } + ] + """.trimIndent() + mockServer + .`when`(request().withPath("/repos/$owner/$name")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody(repositoryResponse), + ) + mockServer + .`when`(request().withPath("/repos/$owner/$name/git/matching-refs/tags/v")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody(tagsResponse), + ) + mockServer + .`when`(request().withPath("/repos/$owner/$name/git/matching-refs/heads/v")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody(headsResponse), + ) // When val versionsOrError = @@ -38,18 +222,25 @@ class GithubApiTest : ) // Then - versionsOrError shouldBeSuccess + versionsOrError shouldBe listOf( Version("v1.0.0"), Version("v1.0.1"), Version("v1"), Version("v2"), - ) + ).right() } test("error occurs when fetching branches and tags") { // Given - // No mocks setup (will fail) + mockServer + .`when`(request()) + .respond( + response() + .withStatusCode(403) + .withHeader("Content-Type", "application/json") + .withBody("""{"message": "There was a problem!"}"""), + ) // When val versionOrError = @@ -61,46 +252,13 @@ class GithubApiTest : ) // Then - versionOrError.shouldBeFailure() + versionOrError shouldBe + ( + "Unexpected response when fetching refs from " + + "http://localhost:${mockServer.port}/" + + "repos/some-owner/some-name/git/matching-refs/tags/v. " + + "Status: 403 Forbidden, response: {\"message\": \"There was a problem!\"}" + ).left() } }, ) - -private fun ClientAndServer.mockHeadsResponse( - owner: String, - name: String, -) { - mockResponse("/repos/$owner/$name/git/refs/heads", "heads.json") -} - -private fun ClientAndServer.mockTagsResponse( - owner: String, - name: String, -) { - mockResponse("/repos/$owner/$name/git/refs/tags", "tags.json") -} - -private fun ClientAndServer.mockRepositoryResponse( - owner: String, - name: String, -) { - mockResponse("/repos/$owner/$name", "repository.json") -} - -private fun ClientAndServer.mockResponse( - path: String, - resource: String, -) { - `when`(request().withPath(path)) - .respond( - response() - .withStatusCode(200) - .withHeader("Content-Type", "application/json") - .withBody(readResource(resource)), - ) -} - -fun readResource(path: String) = - GithubApiTest::class.java.classLoader - .getResourceAsStream(path)!! - .readBytes() From e0a4bf387b647121f6330fd28544af2e1fde67d7 Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Sat, 17 May 2025 22:14:11 +0200 Subject: [PATCH 3/4] Delete resources --- shared-internal/src/test/resources/heads.json | 22 ---- .../src/test/resources/repository.json | 110 ------------------ shared-internal/src/test/resources/tags.json | 22 ---- 3 files changed, 154 deletions(-) delete mode 100644 shared-internal/src/test/resources/heads.json delete mode 100644 shared-internal/src/test/resources/repository.json delete mode 100644 shared-internal/src/test/resources/tags.json diff --git a/shared-internal/src/test/resources/heads.json b/shared-internal/src/test/resources/heads.json deleted file mode 100644 index 82630a78f..000000000 --- a/shared-internal/src/test/resources/heads.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "ref":"refs/heads/v1", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvc2lsZW50LXJldi1wYXJzZQ==", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v1", - "object": { - "sha":"af5130cb8882054eda385840657dcbd1e19ab8f4", - "type":"commit", - "url":"https://api.github.com/repos/some-owner/some-name/git/commits/af5130cb8882054eda385840657dcbd1e19ab8f4" - } - }, - { - "ref":"refs/heads/v2", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvaGVhZHMvdm1qb3NlcGgvdG9vbGtpdC13aW5kb3dzLWV4ZWM=", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/heads/v2", - "object": { - "sha":"c22ccee38a13e34cb01a103c324adb1db665821e", - "type":"commit", - "url":"https://api.github.com/repos/some-owner/some-name/git/commits/c22ccee38a13e34cb01a103c324adb1db665821e" - } - } -] \ No newline at end of file diff --git a/shared-internal/src/test/resources/repository.json b/shared-internal/src/test/resources/repository.json deleted file mode 100644 index bc3c1ea57..000000000 --- a/shared-internal/src/test/resources/repository.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "id": 429460367, - "node_id": "R_kgDOGZkLjw", - "name": "some-name", - "full_name": "some-owner/some-name", - "private": false, - "owner": { - "login": "some-owner", - "id": 1577251, - "node_id": "MDQ6VXNlcjE1NzcyNTE=", - "avatar_url": "https://avatars.githubusercontent.com/u/1577251?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/LeoColman", - "html_url": "https://github.com/LeoColman", - "followers_url": "https://api.github.com/users/LeoColman/followers", - "following_url": "https://api.github.com/users/LeoColman/following{/other_user}", - "gists_url": "https://api.github.com/users/LeoColman/gists{/gist_id}", - "starred_url": "https://api.github.com/users/LeoColman/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/LeoColman/subscriptions", - "organizations_url": "https://api.github.com/users/LeoColman/orgs", - "repos_url": "https://api.github.com/users/LeoColman/repos", - "events_url": "https://api.github.com/users/LeoColman/events{/privacy}", - "received_events_url": "https://api.github.com/users/LeoColman/received_events", - "type": "User", - "user_view_type": "public", - "site_admin": false - }, - "html_url": "https://github.com/LeoColman/MyStack", - "description": null, - "fork": false, - "url": "https://api.github.com/repos/LeoColman/MyStack", - "forks_url": "https://api.github.com/repos/LeoColman/MyStack/forks", - "keys_url": "https://api.github.com/repos/LeoColman/MyStack/keys{/key_id}", - "collaborators_url": "https://api.github.com/repos/LeoColman/MyStack/collaborators{/collaborator}", - "teams_url": "https://api.github.com/repos/LeoColman/MyStack/teams", - "hooks_url": "https://api.github.com/repos/LeoColman/MyStack/hooks", - "issue_events_url": "https://api.github.com/repos/LeoColman/MyStack/issues/events{/number}", - "events_url": "https://api.github.com/repos/LeoColman/MyStack/events", - "assignees_url": "https://api.github.com/repos/LeoColman/MyStack/assignees{/user}", - "branches_url": "https://api.github.com/repos/LeoColman/MyStack/branches{/branch}", - "tags_url": "https://api.github.com/repos/LeoColman/MyStack/tags", - "blobs_url": "https://api.github.com/repos/LeoColman/MyStack/git/blobs{/sha}", - "git_tags_url": "https://api.github.com/repos/LeoColman/MyStack/git/tags{/sha}", - "git_refs_url": "https://api.github.com/repos/LeoColman/MyStack/git/refs{/sha}", - "trees_url": "https://api.github.com/repos/LeoColman/MyStack/git/trees{/sha}", - "statuses_url": "https://api.github.com/repos/LeoColman/MyStack/statuses/{sha}", - "languages_url": "https://api.github.com/repos/LeoColman/MyStack/languages", - "stargazers_url": "https://api.github.com/repos/LeoColman/MyStack/stargazers", - "contributors_url": "https://api.github.com/repos/LeoColman/MyStack/contributors", - "subscribers_url": "https://api.github.com/repos/LeoColman/MyStack/subscribers", - "subscription_url": "https://api.github.com/repos/LeoColman/MyStack/subscription", - "commits_url": "https://api.github.com/repos/LeoColman/MyStack/commits{/sha}", - "git_commits_url": "https://api.github.com/repos/LeoColman/MyStack/git/commits{/sha}", - "comments_url": "https://api.github.com/repos/LeoColman/MyStack/comments{/number}", - "issue_comment_url": "https://api.github.com/repos/LeoColman/MyStack/issues/comments{/number}", - "contents_url": "https://api.github.com/repos/LeoColman/MyStack/contents/{+path}", - "compare_url": "https://api.github.com/repos/LeoColman/MyStack/compare/{base}...{head}", - "merges_url": "https://api.github.com/repos/LeoColman/MyStack/merges", - "archive_url": "https://api.github.com/repos/LeoColman/MyStack/{archive_format}{/ref}", - "downloads_url": "https://api.github.com/repos/LeoColman/MyStack/downloads", - "issues_url": "https://api.github.com/repos/LeoColman/MyStack/issues{/number}", - "pulls_url": "https://api.github.com/repos/LeoColman/MyStack/pulls{/number}", - "milestones_url": "https://api.github.com/repos/LeoColman/MyStack/milestones{/number}", - "notifications_url": "https://api.github.com/repos/LeoColman/MyStack/notifications{?since,all,participating}", - "labels_url": "https://api.github.com/repos/LeoColman/MyStack/labels{/name}", - "releases_url": "https://api.github.com/repos/LeoColman/MyStack/releases{/id}", - "deployments_url": "https://api.github.com/repos/LeoColman/MyStack/deployments", - "created_at": "2021-11-18T14:26:50Z", - "updated_at": "2025-04-23T19:38:18Z", - "pushed_at": "2025-04-23T19:38:15Z", - "git_url": "git://github.com/LeoColman/MyStack.git", - "ssh_url": "git@github.com:LeoColman/MyStack.git", - "clone_url": "https://github.com/LeoColman/MyStack.git", - "svn_url": "https://github.com/LeoColman/MyStack", - "homepage": null, - "size": 24074, - "stargazers_count": 1, - "watchers_count": 1, - "language": null, - "has_issues": true, - "has_projects": false, - "has_downloads": true, - "has_wiki": false, - "has_pages": false, - "has_discussions": false, - "forks_count": 1, - "mirror_url": null, - "archived": false, - "disabled": false, - "open_issues_count": 1, - "license": { - "key": "mit", - "name": "MIT License", - "spdx_id": "MIT", - "url": "https://api.github.com/licenses/mit", - "node_id": "MDc6TGljZW5zZTEz" - }, - "allow_forking": true, - "is_template": false, - "web_commit_signoff_required": false, - "topics": [], - "visibility": "public", - "forks": 1, - "open_issues": 1, - "watchers": 1, - "default_branch": "main", - "temp_clone_token": null, - "network_count": 1, - "subscribers_count": 1 -} \ No newline at end of file diff --git a/shared-internal/src/test/resources/tags.json b/shared-internal/src/test/resources/tags.json deleted file mode 100644 index d0eaa71e9..000000000 --- a/shared-internal/src/test/resources/tags.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "ref":"refs/tags/v1.0.0", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1", - "object": { - "sha":"544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9", - "type":"tag", - "url":"https://api.github.com/repos/actions/some-name/git/tags/544eadc6bf3d226fd7a7a9f0dc5b5bf7ca0675b9" - } - }, - { - "ref":"refs/tags/v1.0.1", - "node_id":"MDM6UmVmMTk3ODE0NjI5OnJlZnMvdGFncy92MQ==", - "url":"https://api.github.com/repos/some-owner/some-name/git/refs/tags/v1.0.1", - "object": { - "sha":"af513c7a016048ae468971c52ed77d9562c7c819", - "type":"tag", - "url":"https://api.github.com/repos/actions/some-name/git/tags/af513c7a016048ae468971c52ed77d9562c7c819" - } - } -] \ No newline at end of file From f2f9d8bd42fa53dcf0db0be720d3b11b9c4c7b34 Mon Sep 17 00:00:00 2001 From: Piotr Krzeminski Date: Sat, 17 May 2025 22:14:51 +0200 Subject: [PATCH 4/4] Revert changes in MavenMetadataBuilding --- .../mavenbinding/MavenMetadataBuilding.kt | 12 ++-- .../mavenbinding/MavenMetadataBuildingTest.kt | 72 +++++++++---------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt index d549ff68c..ef1fe1499 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuilding.kt @@ -1,23 +1,23 @@ package io.github.typesafegithub.workflows.mavenbinding +import arrow.core.Either import arrow.core.getOrElse import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL +import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions import io.github.typesafegithub.workflows.shared.internal.model.Version -import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import io.github.typesafegithub.workflows.shared.internal.fetchAvailableVersions as defaultFetchAvailableVersions private val logger = logger { } internal suspend fun ActionCoords.buildMavenMetadataFile( githubAuthToken: String, - fetchAvailableVersions: ( + fetchAvailableVersions: suspend ( owner: String, name: String, - githubAuthToken: String, - ) -> Result> = ::defaultFetchAvailableVersions, + githubAuthToken: String?, + ) -> Either> = ::fetchAvailableVersions, prefetchBindingArtifacts: (Collection) -> Unit = {}, ): String? { val availableVersions = @@ -31,7 +31,7 @@ internal suspend fun ActionCoords.buildMavenMetadataFile( val lastUpdated = DateTimeFormatter .ofPattern("yyyyMMddHHmmss") - .format(newest.getReleaseDate() ?: ZonedDateTime.now()) + .format(newest.getReleaseDate()) return """ diff --git a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt index 07009d9c6..f8b56c005 100644 --- a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt +++ b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/MavenMetadataBuildingTest.kt @@ -1,5 +1,7 @@ package io.github.typesafegithub.workflows.mavenbinding +import arrow.core.Either +import arrow.core.right import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL @@ -20,19 +22,17 @@ class MavenMetadataBuildingTest : test("various kinds of versions available") { // Given - val fetchAvailableVersions: (String, String, String?) -> Result> = { _, _, _ -> - Result.success( - listOf( - Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), - Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), - Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), - Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - ), - ) + val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { _, _, _ -> + listOf( + Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), + Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), + Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), + Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + ).right() } val xml = @@ -62,16 +62,14 @@ class MavenMetadataBuildingTest : test("no major versions") { // Given - val fetchAvailableVersions: (String, String, String?) -> Result> = { _, _, _ -> - Result.success( - listOf( - Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), - Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - ), - ) + val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { _, _, _ -> + listOf( + Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), + Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + ).right() } val xml = @@ -85,8 +83,8 @@ class MavenMetadataBuildingTest : test("no versions available") { // Given - val fetchAvailableVersions: (String, String, String?) -> Result> = { _, _, _ -> - Result.success(emptyList()) + val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { _, _, _ -> + emptyList().right() } val xml = @@ -101,19 +99,17 @@ class MavenMetadataBuildingTest : (SignificantVersion.entries - FULL).forEach { significantVersion -> test("significant version $significantVersion requested") { // Given - val fetchAvailableVersions: (String, String, String?) -> Result> = { owner, name, _ -> - Result.success( - listOf( - Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), - Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), - Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), - Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), - Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), - ), - ) + val fetchAvailableVersions: suspend (String, String, String?) -> Either> = { owner, name, _ -> + listOf( + Version(version = "v3-beta", dateProvider = { ZonedDateTime.parse("2024-07-01T00:00:00Z") }), + Version(version = "v2", dateProvider = { ZonedDateTime.parse("2024-05-01T00:00:00Z") }), + Version(version = "v1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.1.0", dateProvider = { ZonedDateTime.parse("2024-03-07T00:00:00Z") }), + Version(version = "v1.0.1", dateProvider = { ZonedDateTime.parse("2024-03-05T00:00:00Z") }), + Version(version = "v1.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + Version(version = "v1.0.0", dateProvider = { ZonedDateTime.parse("2024-03-01T00:00:00Z") }), + ).right() } val xml =