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 c2d8912cfd..30de8a72cc 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 @@ -2,143 +2,30 @@ 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.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?, githubEndpoint: String = "https://api.github.com", ): Either> = either { - buildHttpClient().use { httpClient -> - return listOf( - apiTagsUrl(githubEndpoint = githubEndpoint, owner = owner, name = name), - apiBranchesUrl(githubEndpoint = githubEndpoint, owner = owner, name = name), - ).flatMap { url -> fetchGithubRefs(url, githubAuthToken, httpClient).bind() } - .versions(githubAuthToken) - } - } - -private fun List.versions(githubAuthToken: String?): Either> = - either { - this@versions.map { githubRef -> - val version = githubRef.ref.substringAfterLast("/") - Version(version) { - val response = - buildHttpClient().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) { + val github = + GitHubBuilder() + .withEndpoint(githubEndpoint) + .also { if (githubAuthToken != null) { - bearerAuth(githubAuthToken) + it.withOAuthToken(githubAuthToken) } - } - ensure(response.status.isSuccess()) { - "Unexpected response when fetching refs from $url. " + - "Status: ${response.status}, response: ${response.bodyAsText()}" - } - response.body() - } + }.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) } -private fun apiTagsUrl( - githubEndpoint: String, - owner: String, - name: String, -): String = "$githubEndpoint/repos/$owner/$name/git/matching-refs/tags/v" - -private fun apiBranchesUrl( - githubEndpoint: String, - owner: String, - name: String, -): String = "$githubEndpoint/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() = - HttpClient { - val klogger = logger - install(Logging) { - logger = - object : Logger { - override fun log(message: String) { - klogger.trace { message } - } - } - level = ALL - } - install(ContentNegotiation) { - json( - Json { - ignoreUnknownKeys = true - }, - ) - } + apiTags + apiHeads } + +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 eeaa8309b0..fdd9533974 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 @@ -24,6 +24,119 @@ class GithubApiTest : test("branches with major versions and tags with other versions") { // Given + 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 = """ [ @@ -74,6 +187,14 @@ class GithubApiTest : } ] """.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(