diff --git a/.github/workflows/test-script-consuming-jit-bindings.main.do-not-compile.kts b/.github/workflows/test-script-consuming-jit-bindings.main.do-not-compile.kts index 008cb0c4b8..f4d4a83431 100755 --- a/.github/workflows/test-script-consuming-jit-bindings.main.do-not-compile.kts +++ b/.github/workflows/test-script-consuming-jit-bindings.main.do-not-compile.kts @@ -24,14 +24,18 @@ // Always untyped action. @file:DependsOn("typesafegithub:always-untyped-action-for-tests:v1") -// Action version pinned to a commit. +// Action version pinned to an explicit commit. @file:DependsOn("actions:setup-python___commit_lenient:v6.0.0__e797f83bcb11b83ae66e0230d6156d7c80228e7c") +// Action version pinned to a commit. +@file:DependsOn("actions:github-script___commit:v9.0.0") + import io.github.typesafegithub.workflows.actions.actions.Cache import io.github.typesafegithub.workflows.actions.actions.Checkout +import io.github.typesafegithub.workflows.actions.actions.Checkout_Untyped +import io.github.typesafegithub.workflows.actions.actions.GithubScript import io.github.typesafegithub.workflows.actions.actions.SetupNode import io.github.typesafegithub.workflows.actions.actions.SetupPython -import io.github.typesafegithub.workflows.actions.actions.Checkout_Untyped import io.github.typesafegithub.workflows.actions.gradle.ActionsSetupGradle import io.github.typesafegithub.workflows.actions.gradle.ActionsDependencySubmission_Untyped import io.github.typesafegithub.workflows.actions.gradle.ActionsWrapperValidation @@ -53,3 +57,4 @@ ActionsWrapperValidation().actionVersion shouldBe "v4.2" Checkout(fetchTags = false).copy(fetchTags = true) SetupPython().actionVersion shouldBe "e797f83bcb11b83ae66e0230d6156d7c80228e7c" +GithubScript(script = "").actionVersion shouldBe "3a2844b7e9c422d3c10d287c895573f7108da1b3" diff --git a/action-binding-generator/api/action-binding-generator.api b/action-binding-generator/api/action-binding-generator.api index 9dc904e866..d01c2911f5 100644 --- a/action-binding-generator/api/action-binding-generator.api +++ b/action-binding-generator/api/action-binding-generator.api @@ -69,6 +69,7 @@ public final class io/github/typesafegithub/workflows/actionbindinggenerator/dom } public final class io/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion : java/lang/Enum { + public static final field COMMIT Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion; public static final field COMMIT_LENIENT Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion; public static final field FULL Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion; public static final field MAJOR Lio/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion; diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt index 4a3518509a..1495212b27 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/ActionCoords.kt @@ -1,5 +1,6 @@ package io.github.typesafegithub.workflows.actionbindinggenerator.domain +import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT_LENIENT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL @@ -38,7 +39,11 @@ public val ActionCoords.prettyPrint: String get() = "$prettyPrintWithoutVersion@ public val ActionCoords.prettyPrintWithoutVersion: String get() = "$owner/$fullName${ significantVersion.takeUnless { it == FULL }?.let { " with $it version" } ?: "" }${ - if ((significantVersion == COMMIT_LENIENT) && (comment != null)) " and comment '$comment'" else "" + if (((significantVersion == COMMIT_LENIENT) || (significantVersion == COMMIT)) && (comment != null)) { + " and comment '$comment'" + } else { + "" + } }" /** diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion.kt index 379ac02690..b1f2c2970b 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/domain/SignificantVersion.kt @@ -17,10 +17,15 @@ public enum class SignificantVersion { MINOR, /** - * Write the commit ID as the version to the generated YAML. + * Write the given commit ID as the version to the generated YAML. */ COMMIT_LENIENT, + /** + * Write the commit ID as the version to the generated YAML. + */ + COMMIT, + /** * Write the full version to the generated YAML. */ diff --git a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt index 4bd81df904..da42158a1c 100644 --- a/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt +++ b/action-binding-generator/src/main/kotlin/io/github/typesafegithub/workflows/actionbindinggenerator/generation/Generation.kt @@ -18,6 +18,7 @@ import com.squareup.kotlinpoet.buildCodeBlock import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionTypings import io.github.typesafegithub.workflows.actionbindinggenerator.domain.MetadataRevision +import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT_LENIENT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.MAJOR @@ -466,7 +467,7 @@ private fun TypeSpec.Builder.inheritsFromRegularAction( when (coords.significantVersion) { MAJOR -> coords.version.majorVersion MINOR -> coords.version.minorVersion - COMMIT_LENIENT, FULL -> coords.version + COMMIT_LENIENT, COMMIT, FULL -> coords.version }, ).also { if (coords.comment != null) { diff --git a/docs/user-guide/using-actions.md b/docs/user-guide/using-actions.md index f88ea7208b..962fd665e0 100644 --- a/docs/user-guide/using-actions.md +++ b/docs/user-guide/using-actions.md @@ -15,7 +15,7 @@ To add a dependency on an action: 2. Add a dependency on a Maven artifact, e.g. for `actions/checkout@v3` the right way to add the dependency in the script is: `@file:DependsOn("actions:checkout:v3")`. As you can see, the group ID was adopted to model the action's - owner, the artifact ID models the action name, and the version is just action's version (a tag or a branch + owner, the artifact ID models the action name, and the version is just the action's version (a tag or a branch corresponding to a released version). If an action's manifest is defined in a subdirectory, like the popular `gradle/actions/setup-gradle@v3`, replace the slashes in the action name with `__`, so in this case it would be `@file:DependsOn("gradle:actions__setup-gradle:v3")`. @@ -54,6 +54,29 @@ To add a dependency on an action: `[v3,v4)`. In such a case - or always, to be on the safe side - you might want to change the range to `[v3,v4-alpha)`, as the `alpha` version is the lowest possible version in Maven semantics. + ??? tip "Pinning actions to a commit hash" + Past supply-chain attacks where action version tags or branches were repointed to different commits, + either broken ones or malicious ones, or just gone for good, showed that it might be a good idea to instead + pin used actions to a specific commit hash. + + This project supports pinning to a commit hash in two ways. + + Like with `___major` and `___minor` suffixes, described above, you can use the suffix `___commit` to pin the + used action to the commit of the given version. Do **not** combine this with an intentionally changing version, + so do not use it with `v4` or `v4.1` or a version range, but always just with a concrete version that will not + change over time, like `v4.0.0`. The generated YAML will contain the commit hash and thus continue using that + state even if the version points somewhere else. With the consistency check enabled (the default), you will + be notified if the version points to a different commit than the one used in the generated YAML as the job will + fail, so you can notify the action maintainer about the potential attack. + + Additionally, you can use any arbitrary commit hash by using the suffix `___commit_lenient`. With this suffix, + the version also becomes two-part, separated by two underscores. The first part is the version where the typings + will be used from in the typing catalog if the action does not contain typings natively, and it will be put in + the comment after the commit hash. The second part is the commit hash itself. So the version would look like + `v4.0.0__abcdef1234567890abcdef1234567890abcdef12`. The lenient part is, that there is no check at any time that + the version and the commit match in any way. This way you can also use any state of the given action, as long as + the commit contains the action code and you are not restricted to actually labeled commits. + 3. Use the action by importing a class like `io.github.typesafegithub.workflows.actions.actions.Checkout`. For every action, a binding will be generated. However, some less popular actions don't have typings configured for diff --git a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt index 77ea514214..9a76b78ce1 100644 --- a/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt +++ b/jit-binding-server/src/main/kotlin/io/github/typesafegithub/workflows/jitbindingserver/Main.kt @@ -67,7 +67,12 @@ fun main() { } fun Application.appModule( - buildVersionArtifacts: suspend (BindingsServerRequest, HttpClient) -> VersionArtifacts?, + buildVersionArtifacts: suspend ( + BindingsServerRequest, + String, + MeterRegistry, + HttpClient, + ) -> VersionArtifacts?, buildPackageArtifacts: suspend ( BindingsServerRequest, String, @@ -90,7 +95,7 @@ fun Application.appModule( execute(request) } } - val bindingsCache = buildBindingsCache(buildVersionArtifacts, httpClient) + val bindingsCache = buildBindingsCache(buildVersionArtifacts, getGithubAuthToken, httpClient) val metadataCache = buildMetadataCache(bindingsCache, buildPackageArtifacts, getGithubAuthToken) installPlugins(prometheusRegistry) @@ -103,14 +108,29 @@ fun Application.appModule( } private fun buildBindingsCache( - buildVersionArtifacts: suspend (BindingsServerRequest, HttpClient) -> VersionArtifacts?, + buildVersionArtifacts: suspend ( + BindingsServerRequest, + String, + MeterRegistry, + HttpClient, + ) -> VersionArtifacts?, + getGithubAuthToken: () -> String, httpClient: HttpClient, ): LoadingCache = Caffeine .newBuilder() .refreshAfterWrite(1.hours) .recordStats() - .asLoadingCache { Optional.ofNullable(buildVersionArtifacts(it, httpClient)) } + .asLoadingCache { + Optional.ofNullable( + buildVersionArtifacts( + it, + getGithubAuthToken(), + prometheusRegistry, + httpClient, + ), + ) + } @Suppress("ktlint:standard:function-signature") // Conflict with detekt. private fun buildMetadataCache( diff --git a/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt b/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt index 9827791493..1ad3f0ef23 100644 --- a/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt +++ b/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/ArtifactRoutesTest.kt @@ -14,6 +14,7 @@ import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.server.testing.testApplication +import io.micrometer.core.instrument.MeterRegistry import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -26,7 +27,7 @@ class ArtifactRoutesTest : // Given application { appModule( - buildVersionArtifacts = { _, _ -> + buildVersionArtifacts = { _, _, _, _ -> VersionArtifacts( files = mapOf("some-action-v4.pom" to TextArtifact { "Some POM contents" }), typingActualSource = TypingActualSource.TYPING_CATALOG, @@ -52,7 +53,7 @@ class ArtifactRoutesTest : // Given application { appModule( - buildVersionArtifacts = { _, _ -> null }, + buildVersionArtifacts = { _, _, _, _ -> null }, // Irrelevant for these tests. buildPackageArtifacts = { _, _, _, _ -> emptyMap() }, getGithubAuthToken = { "" }, @@ -70,8 +71,9 @@ class ArtifactRoutesTest : test("when artifacts is not available, two requests in a row") { testApplication { // Given - val mockBuildVersionArtifacts = mockk<(BindingsServerRequest, HttpClient) -> VersionArtifacts?>() - every { mockBuildVersionArtifacts(any(), any()) } returns null + val mockBuildVersionArtifacts = + mockk<(BindingsServerRequest, String, MeterRegistry, HttpClient) -> VersionArtifacts?>() + every { mockBuildVersionArtifacts(any(), any(), any(), any()) } returns null application { appModule( buildVersionArtifacts = mockBuildVersionArtifacts, @@ -93,7 +95,7 @@ class ArtifactRoutesTest : // The fact that the resource doesn't exist is cached, and the // resource generation logic isn't called in the second request. - verify(exactly = 1) { mockBuildVersionArtifacts(any(), any()) } + verify(exactly = 1) { mockBuildVersionArtifacts(any(), any(), any(), any()) } } } @@ -102,7 +104,7 @@ class ArtifactRoutesTest : // Given application { appModule( - buildVersionArtifacts = { _, _ -> error("An internal error occurred!") }, + buildVersionArtifacts = { _, _, _, _ -> error("An internal error occurred!") }, // Irrelevant for these tests. buildPackageArtifacts = { _, _, _, _ -> emptyMap() }, getGithubAuthToken = { "" }, @@ -120,8 +122,9 @@ class ArtifactRoutesTest : test("when binding generation fails and then succeeds, and two requests are made") { testApplication { // Given - val mockBuildVersionArtifacts = mockk<(BindingsServerRequest, HttpClient) -> VersionArtifacts?>() - every { mockBuildVersionArtifacts(any(), any()) } throws + val mockBuildVersionArtifacts = + mockk<(BindingsServerRequest, String, MeterRegistry, HttpClient) -> VersionArtifacts?>() + every { mockBuildVersionArtifacts(any(), any(), any(), any()) } throws Exception("An internal error occurred!") andThen VersionArtifacts( files = mapOf("some-action-v4.pom" to TextArtifact { "Some POM contents" }), @@ -146,7 +149,7 @@ class ArtifactRoutesTest : // Then response2.status shouldBe HttpStatusCode.OK - verify(exactly = 2) { mockBuildVersionArtifacts(any(), any()) } + verify(exactly = 2) { mockBuildVersionArtifacts(any(), any(), any(), any()) } } } @@ -155,9 +158,11 @@ class ArtifactRoutesTest : // Given application { appModule( - buildVersionArtifacts = { bindingsServerRequest, _ -> + buildVersionArtifacts = { bindingsServerRequest, _, _, _ -> buildVersionArtifacts( bindingsServerRequest, + "", + mockk(), HttpClient( MockEngine { request -> when (request.url.toString()) { diff --git a/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutesTest.kt b/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutesTest.kt index 6f4f2d1d4e..eec5d5ef09 100644 --- a/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutesTest.kt +++ b/jit-binding-server/src/test/kotlin/io/github/typesafegithub/workflows/jitbindingserver/MetadataRoutesTest.kt @@ -26,7 +26,7 @@ class MetadataRoutesTest : }, getGithubAuthToken = { "some-token" }, // Irrelevant for these tests. - buildVersionArtifacts = { _, _ -> + buildVersionArtifacts = { _, _, _, _ -> VersionArtifacts( files = emptyMap(), typingActualSource = null, @@ -54,7 +54,7 @@ class MetadataRoutesTest : }, getGithubAuthToken = { "some-token" }, // Irrelevant for these tests. - buildVersionArtifacts = { _, _ -> + buildVersionArtifacts = { _, _, _, _ -> VersionArtifacts( files = emptyMap(), typingActualSource = null, @@ -81,7 +81,7 @@ class MetadataRoutesTest : }, getGithubAuthToken = { "some-token" }, // Irrelevant for these tests. - buildVersionArtifacts = { _, _ -> + buildVersionArtifacts = { _, _, _, _ -> VersionArtifacts( files = emptyMap(), typingActualSource = null, @@ -118,7 +118,7 @@ class MetadataRoutesTest : buildPackageArtifacts = mockBuildPackageArtifacts, getGithubAuthToken = { "some-token" }, // Irrelevant for these tests. - buildVersionArtifacts = { _, _ -> + buildVersionArtifacts = { _, _, _, _ -> VersionArtifacts( files = emptyMap(), typingActualSource = null, diff --git a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt index 55df3dc892..8ddd8d2999 100644 --- a/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt +++ b/maven-binding-builder/src/main/kotlin/io/github/typesafegithub/workflows/mavenbinding/VersionArtifactsBuilding.kt @@ -1,7 +1,10 @@ package io.github.typesafegithub.workflows.mavenbinding +import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.TypingActualSource +import io.github.typesafegithub.workflows.shared.internal.fetchVersionSha import io.ktor.client.HttpClient +import io.micrometer.core.instrument.MeterRegistry sealed interface Artifact @@ -20,8 +23,28 @@ data class VersionArtifacts( suspend fun buildVersionArtifacts( bindingsServerRequest: BindingsServerRequest, + githubAuthToken: String, + meterRegistry: MeterRegistry, httpClient: HttpClient, ): VersionArtifacts? { + val bindingsServerRequest = + if (bindingsServerRequest.actionCoords.significantVersion == COMMIT) { + bindingsServerRequest.copy( + actionCoords = + bindingsServerRequest.actionCoords.copy( + version = + fetchVersionSha( + "${bindingsServerRequest.actionCoords.owner}/${bindingsServerRequest.actionCoords.name}", + bindingsServerRequest.actionCoords.version, + githubAuthToken, + meterRegistry, + ).getOrNull() ?: return null, + comment = bindingsServerRequest.actionCoords.version, + ), + ) + } else { + bindingsServerRequest + } with(bindingsServerRequest) { val jars = actionCoords.buildJars(httpClient = httpClient) ?: return null val pom = this.buildPomFile() diff --git a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuildingTest.kt b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuildingTest.kt index 8c53af52e5..89bb4c9fa3 100644 --- a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuildingTest.kt +++ b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/ModuleBuildingTest.kt @@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.mavenbinding import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion +import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT_LENIENT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL import io.kotest.core.spec.style.FunSpec @@ -12,9 +13,10 @@ class ModuleBuildingTest : SignificantVersion.entries.forEach { significantVersion -> test("significant version $significantVersion requested") { val commitLenient = significantVersion == COMMIT_LENIENT + val commitOrCommitLenient = (significantVersion == COMMIT) || commitLenient var nameSuffix = if (significantVersion == FULL) "" else "___$significantVersion" var versionSuffix = if (commitLenient) "__commit-sha" else "" - var version = if (commitLenient) "commit-sha" else "v1.2.3" + var version = if (commitOrCommitLenient) "commit-sha" else "v1.2.3" val bindingsServerRequest = BindingsServerRequest( @@ -25,7 +27,7 @@ class ModuleBuildingTest : owner = "owner", name = "name", version = version, - comment = if (commitLenient) "v1.2.3" else null, + comment = if (commitOrCommitLenient) "v1.2.3" else null, significantVersion = significantVersion, ), ) diff --git a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuildingTest.kt b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuildingTest.kt index 2a16652abb..b9440e8437 100644 --- a/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuildingTest.kt +++ b/maven-binding-builder/src/test/kotlin/io/github/typesafegithub/workflows/mavenbinding/PomBuildingTest.kt @@ -2,6 +2,7 @@ package io.github.typesafegithub.workflows.mavenbinding import io.github.typesafegithub.workflows.actionbindinggenerator.domain.ActionCoords import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion +import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.COMMIT_LENIENT import io.github.typesafegithub.workflows.actionbindinggenerator.domain.SignificantVersion.FULL import io.kotest.core.spec.style.FunSpec @@ -13,15 +14,16 @@ class PomBuildingTest : test("significant version $significantVersion requested") { // given val commitLenient = significantVersion == COMMIT_LENIENT + val commitOrCommitLenient = (significantVersion == COMMIT) || commitLenient var nameSuffix = if (significantVersion == FULL) "" else "___$significantVersion" var versionSuffix = if (commitLenient) "__commit-sha" else "" var prettyPrintSuffix = when (significantVersion) { FULL -> "" - COMMIT_LENIENT -> " with $significantVersion version and comment 'v1.2.3'" + COMMIT_LENIENT, COMMIT -> " with $significantVersion version and comment 'v1.2.3'" else -> " with $significantVersion version" } - var version = if (commitLenient) "commit-sha" else "v1.2.3" + var version = if (commitOrCommitLenient) "commit-sha" else "v1.2.3" val bindingsServerRequest = BindingsServerRequest( @@ -32,7 +34,7 @@ class PomBuildingTest : owner = "owner", name = "name", version = version, - comment = if (commitLenient) "v1.2.3" else null, + comment = if (commitOrCommitLenient) "v1.2.3" else null, significantVersion = significantVersion, ), ) 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 d6728e2aee..bc56c0ef1a 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,8 +1,10 @@ package io.github.typesafegithub.workflows.shared.internal import arrow.core.Either +import arrow.core.left import arrow.core.raise.either import arrow.core.raise.ensure +import arrow.core.right import io.github.oshai.kotlinlogging.KotlinLogging.logger import io.github.typesafegithub.workflows.shared.internal.model.Version import io.ktor.client.HttpClient @@ -28,12 +30,45 @@ private val logger = logger { } private const val MAX_REF_PARTS = 3 +private const val GITHUB_ENDPOINT = "https://api.github.com" + +suspend fun fetchVersionSha( + repo: String, + version: String, + githubAuthToken: String?, + meterRegistry: MeterRegistry? = null, + githubEndpoint: String = GITHUB_ENDPOINT, +): Either = + either { + buildHttpClient(meterRegistry = meterRegistry).use { httpClient -> + return listOf( + apiTagUrl(githubEndpoint = githubEndpoint, repo = repo, version = version), + apiBranchUrl(githubEndpoint = githubEndpoint, repo = repo, version = version), + ).flatMap { url -> fetchGithubRefs(url, githubAuthToken, httpClient).bind() } + .firstOrNull { + val refParts = it.ref.split('/', limit = 4) + (refParts.size == MAX_REF_PARTS) && (refParts[2] == version) + }?.`object` + ?.let { + it.getCommitSha( + httpClient + .get(urlString = it.url) { + if (githubAuthToken != null) { + bearerAuth(githubAuthToken) + } + }, + ) + }?.right() + ?: "Specified version $version not found".left() + } + } + suspend fun fetchAvailableVersions( owner: String, name: String, githubAuthToken: String?, meterRegistry: MeterRegistry? = null, - githubEndpoint: String = "https://api.github.com", + githubEndpoint: String = GITHUB_ENDPOINT, ): Either> = either { buildHttpClient(meterRegistry = meterRegistry).use { httpClient -> @@ -106,12 +141,24 @@ private suspend fun fetchGithubRefs( response.body() } +private fun apiTagUrl( + githubEndpoint: String, + repo: String, + version: String, +): String = "$githubEndpoint/repos/$repo/git/matching-refs/tags/$version" + private fun apiTagsUrl( githubEndpoint: String, owner: String, name: String, ): String = "$githubEndpoint/repos/$owner/$name/git/matching-refs/tags/v" +private fun apiBranchUrl( + githubEndpoint: String, + repo: String, + version: String, +): String = "$githubEndpoint/repos/$repo/git/matching-refs/heads/$version" + private fun apiBranchesUrl( githubEndpoint: String, owner: String,